├── .gitignore ├── Sources ├── SwiftDI │ ├── _SwiftDI.swift │ ├── module.swift │ └── Intramodular │ │ ├── Error Handling │ │ └── _SwiftDI.Error.swift │ │ ├── Core │ │ ├── TaskDependenciesExporting.swift │ │ ├── TaskDependencyValues.swift │ │ ├── TaskDependencies.LookupRequest.swift │ │ ├── TaskDependencyKey.swift │ │ └── TaskDependency-Initializers.swift │ │ └── SwiftUI │ │ └── TaskDependencies+SwiftUI.swift ├── Merge │ ├── Intramodular │ │ ├── Observable Tasks │ │ │ ├── ObservableTasks.swift │ │ │ ├── SwiftUI │ │ │ │ ├── TaskButtonStatus.swift │ │ │ │ ├── TaskButtonConfiguration.swift │ │ │ │ └── Task+EnvironmentValues.swift │ │ │ ├── _ObservableTaskGroup+SwiftUI.swift │ │ │ ├── EmptyObservableTask.swift │ │ │ ├── Foundation │ │ │ │ ├── TaskResultPublisher.swift │ │ │ │ ├── ObservableTaskBase.swift │ │ │ │ ├── ObservableTask.concatenate.swift │ │ │ │ ├── ObservableTasks.Map.swift │ │ │ │ ├── TaskSuccessPublisher.swift │ │ │ │ └── ObservableTasks.HandleEvents.swift │ │ │ ├── _AnyObservableTaskGroup.swift │ │ │ ├── Status │ │ │ │ ├── TaskOutput.swift │ │ │ │ └── ObservableTaskFailure.swift │ │ │ ├── ObservableTask.swift │ │ │ └── ObservableTaskOutputPublisher.swift │ │ ├── App Running State │ │ │ └── AppRunningState.swift │ │ ├── Utilities │ │ │ ├── Concurrency │ │ │ │ ├── _NotMainActor.swift │ │ │ │ ├── _UnsafeActorIsolated.swift │ │ │ │ ├── _KeyedUnsafeThrowingContinuations.swift │ │ │ │ └── _AsyncGenerationBox.swift │ │ │ ├── _SwiftTaskProtocol.swift │ │ │ ├── Mutex │ │ │ │ ├── MutexProtocol.swift │ │ │ │ ├── Semaphore │ │ │ │ │ ├── SemaphoreProtocol.swift │ │ │ │ │ └── AnySemaphore.swift │ │ │ │ ├── Lock │ │ │ │ │ ├── ReadWriteLockProtocol.swift │ │ │ │ │ └── Lock.swift │ │ │ │ └── _MutexProtectedType.swift │ │ │ ├── _MaybeAsyncProtocol.swift │ │ │ ├── Collections │ │ │ │ └── _NaiveDoublyLinkedList.swift │ │ │ └── _MultiReaderSubject.swift │ │ ├── Observation │ │ │ ├── _ObservationRegistrarNotifying.swift │ │ │ ├── _withContinuousObservationTracking.swift │ │ │ └── _RuntimeConditionalObservationTrackedValue.swift │ │ ├── Notifications │ │ │ └── NotificationPublishing.swift │ │ ├── Shell Scripting │ │ │ └── SystemShell.run.swift │ │ ├── Process │ │ │ ├── Process.PipeName.swift │ │ │ ├── _AsyncProcess+Initializers.swift │ │ │ ├── _AsyncProcess._StandardStreamsBuffer.swift │ │ │ └── Process.StandardOutputSink.swift │ │ ├── Rate-limiting & Retrying │ │ │ ├── TaskRetryDelayStrategy.swift │ │ │ └── _TaskRetryPolicy.swift │ │ └── WIP │ │ │ ├── _TaskSinkProtocol.swift │ │ │ └── _AsyncTaskScheduler.swift │ ├── Intermodular │ │ ├── Extensions │ │ │ ├── Combine │ │ │ │ ├── Just++.swift │ │ │ │ ├── Fail++.swift │ │ │ │ ├── Result.Publisher++.swift │ │ │ │ ├── Cancellable++.swift │ │ │ │ ├── ObservableObjectPublisher++.swift │ │ │ │ ├── Published++.swift │ │ │ │ ├── AnyPublisher++.swift │ │ │ │ ├── Publisher+Task.swift │ │ │ │ └── _CombineAsyncStream.swift │ │ │ ├── Foundation │ │ │ │ ├── Thread++.swift │ │ │ │ ├── Operation++.swift │ │ │ │ └── NSLocking++.swift │ │ │ ├── Dispatch │ │ │ │ ├── DispatchTime++.swift │ │ │ │ ├── DispatchQoS.QoSClass++.swift │ │ │ │ ├── DispatchTimeInterval++.swift │ │ │ │ └── DispatchSource++.swift │ │ │ ├── Swift │ │ │ │ └── Duration++.swift │ │ │ └── _Concurrency │ │ │ │ ├── Clock++.swift │ │ │ │ ├── AsyncStream++.swift │ │ │ │ └── AsyncSequence++.swift │ │ ├── Helpers │ │ │ ├── Combine │ │ │ │ ├── Publishers.MapError+.swift │ │ │ │ ├── Publisher.dump.swift │ │ │ │ ├── Publishers.MergeMany+.swift │ │ │ │ ├── CancelableRetain.swift │ │ │ │ ├── Publisher.breakpoint+.swift │ │ │ │ ├── EmptyCancellable.swift │ │ │ │ ├── AnySubscription.swift │ │ │ │ ├── Cancellables.Concatenate.swift │ │ │ │ ├── SinkPublisher.swift │ │ │ │ ├── Publishers.IndexedPublisher.swift │ │ │ │ ├── Publisher.unwrap.swift │ │ │ │ ├── _opaque_ObservableObject.swift │ │ │ │ ├── Publisher.sink+.swift │ │ │ │ ├── _CancellablesProviding.swift │ │ │ │ ├── Publishers.Catch+.swift │ │ │ │ ├── Publisher.join.swift │ │ │ │ ├── Subscriptions.AddCancellable.swift │ │ │ │ ├── Future+DispatchQueue.swift │ │ │ │ ├── PublisherQueue.swift │ │ │ │ ├── PublisherBuilder.swift │ │ │ │ ├── Publishers.MapSubscriber.swift │ │ │ │ ├── Publishers.FlatMapLatest.swift │ │ │ │ ├── SingleAssignmentAnyCancellable.swift │ │ │ │ ├── Publishers.Map+.swift │ │ │ │ ├── Subscribers.MapSubscription.swift │ │ │ │ ├── Publishers.ConcatenateMany.swift │ │ │ │ ├── Publisher.prefixUntilAfter.swift │ │ │ │ ├── AnySubject.swift │ │ │ │ ├── Publishers.While.swift │ │ │ │ ├── _AsyncObjectWillChangePublisher.swift │ │ │ │ ├── AnyFuture.swift │ │ │ │ ├── AnyObservableObject.swift │ │ │ │ ├── RetainUntilCancel.swift │ │ │ │ ├── Publishers.FlatMap+.swift │ │ │ │ ├── _opaque_VoidSender.swift │ │ │ │ ├── AnyObjectWillChangePublisher.swift │ │ │ │ ├── SingleOutputPublisher.subscribeAndWaitUntilDone.swift │ │ │ │ └── MainThreadScheduler.swift │ │ │ ├── Foundation │ │ │ │ ├── OperationQueue+Task.swift │ │ │ │ ├── Operation+Task.swift │ │ │ │ ├── NSLockProtocol.swift │ │ │ │ └── ProcessPublisher-EnvSupport.swift │ │ │ ├── Swift │ │ │ │ └── Optional.UnwrapPublisher.swift │ │ │ ├── Dispatch │ │ │ │ └── DispatchQoS+.swift │ │ │ ├── _Concurrency │ │ │ │ ├── Task.firstResult.swift │ │ │ │ ├── _offTheMainThread.swift │ │ │ │ └── Task.repeat.swift │ │ │ └── Darwin │ │ │ │ ├── OSUnfairLock.swift │ │ │ │ └── DarwinAtomicOperationMemoryOrder.swift │ │ └── Protocol Conformances │ │ │ ├── Foundation │ │ │ └── Foundation+Mutex.swift │ │ │ ├── Combine │ │ │ ├── Combine+Initiable.swift │ │ │ ├── Combine+Hashable.swift │ │ │ └── Combine+Codable.swift │ │ │ ├── Dispatch │ │ │ └── Dispatch+Mutex.swift │ │ │ └── Swallow │ │ │ └── Swallow+Publisher.swift │ ├── module.swift │ └── Intramodular (WIP) │ │ └── Actor Effects │ │ └── _ActorSideEffect.swift ├── ShellScripting │ ├── module.swift │ └── Intramodular │ │ └── PreferredUNIXShell.swift └── CommandLineToolSupport │ └── Intramodular │ ├── CommandLineTool.swift │ ├── Builtins │ ├── CLT.EnvironmentVariableValue.swift │ ├── _CommandLineToolParameter.swift │ ├── _CommandLineToolEnvironmentVariable.swift │ └── CLT.EnvironmentVariable.swift │ └── AnyCommandLineTool.swift ├── Tests ├── PassthroughTaskTests.swift ├── AsyncStreamTests.swift ├── ThrowingTaskQueueTests.swift ├── KeyedThrowingTaskGroupTests.swift └── PublisherExtensionsTests.swift ├── .github └── workflows │ └── preternatural-build.yml ├── LICENSE ├── Package.resolved ├── README.md ├── .swift-format └── Package.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .swiftpm/ 3 | .swiftpm/* 4 | /*.xcodeproj 5 | /.build 6 | /Packages 7 | xcuserdata/ 8 | -------------------------------------------------------------------------------- /Sources/SwiftDI/_SwiftDI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | public enum _SwiftDI { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Sources/SwiftDI/module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import SwallowMacrosClient 7 | import Swift 8 | 9 | #module { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/ObservableTasks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | public struct ObservableTasks { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Combine/Just++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Just { 9 | public init(_ output: () -> Output) { 10 | self.init(output()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Foundation/Thread++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | extension Thread { 9 | public static var _isMainThread: Bool { 10 | isMainThread 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Dispatch/DispatchTime++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Dispatch 6 | 7 | extension DispatchTime { 8 | public var uptimeMilliseconds: UInt64 { 9 | uptimeNanoseconds / 1_000_000 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/SwiftUI/TaskButtonStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | public struct TaskButtonStatus: Hashable { 9 | public let description: ObservableTaskStatusDescription 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publishers.MapError+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Publisher { 9 | public func eraseError() -> Publishers.MapError { 10 | mapError({ $0 as Error }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Protocol Conformances/Foundation/Foundation+Mutex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swallow 7 | 8 | extension NSLock: TestableLock { 9 | 10 | } 11 | 12 | extension NSRecursiveLock: ReentrantLock, TestableLock { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Combine/Fail++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Fail where Output == Any, Failure == Error { 9 | public init(error: Failure) { 10 | self.init(outputType: Any.self, failure: error) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/ShellScripting/module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | @_exported import class Merge.SystemShell 6 | import Foundation 7 | 8 | #if os(macOS) 9 | extension Process { 10 | @available(*, deprecated) 11 | public typealias ShellEnvironment = PreferredUNIXShell.Name 12 | } 13 | #endif 14 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Combine/Result.Publisher++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Result.Publisher where Failure == Swift.Error { 9 | public init(_ output: () throws -> Success) { 10 | self = Result(catching: output).publisher 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SwiftDI/Intramodular/Error Handling/_SwiftDI.Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Diagnostics 6 | import Swallow 7 | 8 | extension _SwiftDI { 9 | public enum Error: Swift.Error { 10 | case failedToResolveDependency(Any.Type) 11 | case failedToConsumeDependencies(AnyError) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/App Running State/AppRunningState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | /// An enum that represents the running state of an iOS, macOS, tvOS or watchOS application. 9 | public enum AppRunningState { 10 | case active 11 | case inactive 12 | case background 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Concurrency/_NotMainActor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | @globalActor 8 | public actor _NotMainActor { 9 | public actor ActorType { 10 | fileprivate init() { 11 | 12 | } 13 | } 14 | 15 | public static let shared: ActorType = ActorType() 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publisher.dump.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Publisher { 9 | /// Dumps the the publisher's output's contents using its mirror. 10 | public func dump() -> Publishers.HandleEvents { 11 | handleOutput({ Swift.dump($0) }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publishers.MergeMany+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Array where Element: Publisher { 9 | /// Creates a merge-many publisher from the array's elements. 10 | public var mergeManyPublisher: Publishers.MergeMany { 11 | .init(self) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Combine/Cancellable++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Combine.Cancellable { 9 | public func eraseToAnyCancellable() -> Combine.AnyCancellable { 10 | Combine.AnyCancellable(self) 11 | } 12 | } 13 | 14 | extension _Concurrency.Task: Combine.Cancellable { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Protocol Conformances/Combine/Combine+Initiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | extension CurrentValueSubject where Output: Swallow.Initiable { 9 | public convenience init() { 10 | self.init(Output()) 11 | } 12 | } 13 | 14 | extension PassthroughSubject: Swallow._ThrowingInitiable, Swallow.Initiable { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Combine/ObservableObjectPublisher++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension ObservableObjectPublisher { 9 | @inlinable 10 | public func publish(to publisher: ObservableObjectPublisher) -> some Publisher { 11 | handleOutput({ [weak publisher] in 12 | publisher?.send() 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftDI/Intramodular/Core/TaskDependenciesExporting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | 7 | /// A type that exports some set of dependencies. 8 | /// 9 | /// These exported dependencies are consumed in operations such as `withTaskDependencies(from: ...) { ... }` etc. 10 | public protocol _TaskDependenciesExporting { 11 | var _exportedTaskDependencies: TaskDependencies { get } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/CancelableRetain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Dispatch 7 | import Swift 8 | 9 | public final class CancellableRetain: Cancellable { 10 | private var value: Value? 11 | 12 | public init(_ value: Value) { 13 | self.value = value 14 | } 15 | 16 | public func cancel() { 17 | value = nil 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/_SwiftTaskProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import _Concurrency 6 | import Swift 7 | 8 | /// A protocol for `_Concurrency.Task` to conform to. 9 | public protocol _SwiftTaskProtocol: Sendable { 10 | associatedtype Success 11 | associatedtype Failure 12 | 13 | func cancel() 14 | } 15 | 16 | extension Task: _SwiftTaskProtocol { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/SwiftUI/TaskButtonConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | import SwiftUI 8 | 9 | public struct TaskButtonConfiguration { 10 | public let label: AnyView 11 | public let isPressed: Bool? 12 | 13 | public let isInterruptible: Bool 14 | public let isRestartable: Bool 15 | 16 | public let status: ObservableTaskStatusDescription 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publisher.breakpoint+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Dispatch 7 | import Swift 8 | 9 | extension Publisher { 10 | public func breakpoint(_ trap: Bool) -> Publishers.Breakpoint { 11 | breakpoint( 12 | receiveSubscription: { _ in trap }, 13 | receiveOutput: { _ in trap }, 14 | receiveCompletion: { _ in trap } 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/EmptyCancellable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | public final class EmptyCancellable: Cancellable { 9 | public init() { 10 | 11 | } 12 | 13 | public func cancel() { 14 | 15 | } 16 | } 17 | 18 | // MARK: - API 19 | 20 | extension AnyCancellable { 21 | public static func empty() -> Self { 22 | .init(EmptyCancellable()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Combine/Published++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Published { 9 | private class PublishedWrapper { 10 | @Published private(set) var value: Value 11 | 12 | init(_ value: Published) { 13 | _value = value 14 | } 15 | } 16 | 17 | @_spi(Internal) 18 | public var _wrappedValue: Value { 19 | PublishedWrapper(self).value 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Foundation/OperationQueue+Task.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | extension OperationQueue { 9 | /// Add a barrier task. 10 | public func addBarrierTask() -> AnyTask { 11 | let result = PassthroughTask() 12 | 13 | self.addBarrierBlock { 14 | result.send(status: .success(())) 15 | } 16 | 17 | return result.eraseToAnyTask() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Mutex/MutexProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | 7 | public protocol MutexProtocol: Sendable { 8 | 9 | } 10 | 11 | public protocol ReentrantMutexProtocol: MutexProtocol { 12 | 13 | } 14 | 15 | // MARK: - Deprecated 16 | 17 | @available(*, deprecated, renamed: "MutexProtocol") 18 | public typealias Mutex = MutexProtocol 19 | @available(*, deprecated, renamed: "ReentrantMutexProtocol") 20 | public typealias ReentrantMutex = ReentrantMutexProtocol 21 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Protocol Conformances/Combine/Combine+Hashable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | extension Published: Swift.Equatable where Value: Swift.Equatable { 9 | public static func == (lhs: Self, rhs: Self) -> Bool { 10 | lhs._wrappedValue == rhs._wrappedValue 11 | } 12 | } 13 | 14 | extension Published: Swift.Hashable where Value: Swift.Hashable { 15 | public func hash(into hasher: inout Hasher) { 16 | _wrappedValue.hash(into: &hasher) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Protocol Conformances/Combine/Combine+Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Combine.Published: Swift.Decodable where Value: Decodable { 9 | public init(from decoder: Decoder) throws { 10 | self.init(wrappedValue: try Value(from: decoder)) 11 | } 12 | } 13 | 14 | extension Combine.Published: Swift.Encodable where Value: Encodable { 15 | public func encode(to encoder: Encoder) throws { 16 | try _wrappedValue.encode(to: encoder) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Mutex/Semaphore/SemaphoreProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | 7 | public protocol SemaphoreProtocol: MutexProtocol { 8 | associatedtype WaitResult 9 | associatedtype SignalResult 10 | 11 | @discardableResult 12 | func wait() -> WaitResult 13 | 14 | @discardableResult 15 | func signal() -> SignalResult 16 | } 17 | 18 | // MARK: - Deprecated 19 | 20 | @available(*, deprecated, renamed: "SemaphoreProtocol") 21 | public typealias Semaphore = SemaphoreProtocol 22 | -------------------------------------------------------------------------------- /Sources/SwiftDI/Intramodular/Core/TaskDependencyValues.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | 7 | public typealias TaskDependencyValues = HeterogeneousDictionary 8 | 9 | extension TaskDependencyValues { 10 | public subscript( 11 | key: Key.Type 12 | ) -> Key.Value { 13 | get { 14 | self[key] ?? key.defaultValue 15 | } set { 16 | self[key as any HeterogeneousDictionaryKey.Type] = newValue 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Mutex/Lock/ReadWriteLockProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | 7 | protocol ReadWriteLockProtocol: Lock { 8 | func acquireOrBlockForReading() 9 | func relinquishForReading() 10 | 11 | func acquireOrBlockForWriting() 12 | func relinquishForWriting() 13 | } 14 | 15 | protocol ReentrantReadWriteLockProtocol: ReentrantLock { 16 | func acquireOrBlockForReading() 17 | func relinquishForReading() 18 | 19 | func acquireOrBlockForWriting() 20 | func relinquishForWriting() 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Foundation/Operation+Task.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | extension Operation { 9 | /// Creates a task that represents this `Operation`. 10 | public func convertToTask() -> AnyTask { 11 | let result = PassthroughTask() 12 | 13 | completionBlock = { 14 | result.send(status: .success(())) 15 | } 16 | 17 | return 18 | result 19 | .handleEvents(receiveStart: { self.start() }) 20 | .eraseToAnyTask() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Swift/Optional.UnwrapPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | extension Optional { 9 | public typealias UnwrapPublisher = Either.UnwrappingError>.Publisher, Combine.Fail> 10 | 11 | @_disfavoredOverload 12 | public var unwrapPublisher: UnwrapPublisher { 13 | guard let wrappedValue = self else { 14 | return .right(Fail(error: .unexpectedlyFoundNil)) 15 | } 16 | 17 | return .left(Just(wrappedValue).setFailureType(to: UnwrappingError.self)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PassthroughTaskTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | @testable import Merge 6 | 7 | import Swallow 8 | import XCTest 9 | 10 | final class PassthroughTaskTests: XCTestCase { 11 | func testStatus() async throws { 12 | let task = PassthroughTask(priority: nil) { 13 | try await Task.sleep(.seconds(1)) 14 | 15 | return 69 16 | } 17 | 18 | XCTAssert(task.status == .inactive) 19 | 20 | task.start() 21 | 22 | XCTAssert(task.status == .active) 23 | 24 | let value = try await task.value 25 | 26 | XCTAssertEqual(value, 69) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Foundation/Operation++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swallow 7 | 8 | extension Foundation.Operation { 9 | public func addCompletionBlock(_ block: @escaping () -> Void) { 10 | if let existing = completionBlock { 11 | completionBlock = { 12 | existing() 13 | block() 14 | } 15 | } else { 16 | completionBlock = block 17 | } 18 | } 19 | 20 | public func addDependencies(_ dependencies: [Foundation.Operation]) { 21 | for dependency in dependencies { 22 | addDependency(dependency) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Swift/Duration++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 9 | extension Swift.Duration { 10 | @_spi(Internal) 11 | public var _timeInterval: TimeInterval { 12 | TimeInterval(components.seconds) + Double(components.attoseconds) / 1e18 13 | } 14 | 15 | @_spi(Internal) 16 | public init(_timeInterval: TimeInterval) { 17 | let fraction = _timeInterval - floor(_timeInterval) 18 | 19 | self.init( 20 | secondsComponent: Int64(_timeInterval), 21 | attosecondsComponent: Int64(fraction * 1e18) 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/preternatural-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: [ master ] 5 | workflow_dispatch: 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | jobs: 10 | preternatural-build: 11 | name: Build (Xcode ${{ matrix.xcode }}) 12 | runs-on: ghcr.io/cirruslabs/macos-runner:sequoia 13 | strategy: 14 | matrix: 15 | xcode: ['16.2', '16.3'] 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v3 19 | - name: Run Preternatural Build 20 | uses: PreternaturalAI/preternatural-github-actions/preternatural-build@main 21 | with: 22 | xcode-version: ${{ matrix.xcode }} 23 | configurations: '["debug"]' -------------------------------------------------------------------------------- /Sources/CommandLineToolSupport/Intramodular/CommandLineTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | /// A type that wraps a command line tool. 9 | @available(macOS 11.0, *) 10 | @available(iOS, unavailable) 11 | @available(macCatalyst, unavailable) 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | public protocol CommandLineTool: AnyCommandLineTool { 15 | associatedtype EnvironmentVariables = _CommandLineTool_DefaultEnvironmentVariables 16 | } 17 | 18 | public enum CommandLineTools { 19 | 20 | } 21 | 22 | // MARK: - Supplementary 23 | 24 | public typealias CLT = CommandLineTools 25 | 26 | // MARK: - Auxiliary 27 | 28 | public struct _CommandLineTool_DefaultEnvironmentVariables { 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/CommandLineToolSupport/Intramodular/Builtins/CLT.EnvironmentVariableValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | extension CLT { 9 | /// A type that can represent the raw value of an environment variable to be passed in a command invocation. 10 | public protocol EnvironmentVariableValue { 11 | 12 | } 13 | } 14 | 15 | extension Optional: CLT.EnvironmentVariableValue where Wrapped: CLT.EnvironmentVariableValue { 16 | 17 | } 18 | 19 | extension Bool: CLT.EnvironmentVariableValue { 20 | 21 | } 22 | 23 | extension Int: CLT.EnvironmentVariableValue { 24 | 25 | } 26 | 27 | extension String: CLT.EnvironmentVariableValue { 28 | 29 | } 30 | 31 | extension URL: CLT.EnvironmentVariableValue { 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Foundation/NSLockProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swallow 7 | 8 | @objc public protocol NSLockProtocol { 9 | func lock() 10 | @objc(tryLock) func `try`() -> Bool 11 | func unlock() 12 | } 13 | 14 | // MARK: - Conformances 15 | 16 | extension NSLockProtocol where Self: TestableLock { 17 | public func acquireOrBlock() { 18 | lock() 19 | } 20 | 21 | public func acquireOrFail() throws { 22 | try `try`().orThrow() 23 | } 24 | 25 | public func relinquish() { 26 | unlock() 27 | } 28 | } 29 | 30 | // MARK: - Conformances 31 | 32 | extension NSLock: NSLockProtocol { 33 | 34 | } 35 | 36 | extension NSRecursiveLock: NSLockProtocol { 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observation/_ObservationRegistrarNotifying.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | #if canImport(Observation) 7 | import Observation 8 | #endif 9 | import Swallow 10 | 11 | public enum _ObservationRegistrarTrackedOperationKind { 12 | case accessOnly 13 | case mutation 14 | } 15 | 16 | /// Adding `@Observable` to a type after importing `Observation` installs an "observation registrar". 17 | /// 18 | /// The types implementing this protocol are expected to expose raw access to registrar-notifying operations. 19 | public protocol _ObservationRegistrarNotifying { 20 | func notifyingObservationRegistrar( 21 | _ kind: _ObservationRegistrarTrackedOperationKind, 22 | perform operation: () -> Result 23 | ) -> Result 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/AnySubscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | public struct AnySubscription: Subscription { 9 | public let combineIdentifier = CombineIdentifier() 10 | 11 | private let cancellable: Cancellable 12 | private let onRequest: (Subscribers.Demand) -> () 13 | 14 | public init( 15 | _ cancellable: Cancellable, 16 | onRequest: @escaping (Subscribers.Demand) -> () = { _ in } 17 | ) { 18 | self.cancellable = cancellable 19 | self.onRequest = onRequest 20 | } 21 | 22 | public func cancel() { 23 | cancellable.cancel() 24 | } 25 | 26 | public func request(_ demand: Subscribers.Demand) { 27 | onRequest(demand) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/CommandLineToolSupport/Intramodular/Builtins/_CommandLineToolParameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swallow 7 | 8 | public protocol _CommandLineToolParameterProtocol: PropertyWrapper { 9 | 10 | } 11 | 12 | @propertyWrapper 13 | public struct _CommandLineToolParameter: _CommandLineToolParameterProtocol { 14 | var _wrappedValue: WrappedValue 15 | 16 | public var wrappedValue: WrappedValue { 17 | get { 18 | _wrappedValue 19 | } set { 20 | _wrappedValue = newValue 21 | } 22 | } 23 | 24 | public init(wrappedValue: WrappedValue) { 25 | self._wrappedValue = wrappedValue 26 | } 27 | } 28 | 29 | extension CommandLineTool { 30 | public typealias Parameter = _CommandLineToolParameter 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Cancellables.Concatenate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Dispatch 7 | import Swift 8 | 9 | extension Cancellables { 10 | public final class Concatenate: Cancellable { 11 | public let prefix: T 12 | public let suffix: U 13 | 14 | public init(prefix: T, suffix: U) { 15 | self.prefix = prefix 16 | self.suffix = suffix 17 | } 18 | 19 | public func cancel() { 20 | prefix.cancel() 21 | suffix.cancel() 22 | } 23 | } 24 | } 25 | 26 | extension Cancellable { 27 | public func concatenate(with other: T) -> Cancellables.Concatenate { 28 | .init(prefix: self, suffix: other) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Mutex/Semaphore/AnySemaphore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | 7 | public final class AnySemaphore: SemaphoreProtocol { 8 | public let base: any Sendable 9 | 10 | private let signalImpl: @Sendable (Any) -> (() -> Any) 11 | private let waitImpl: @Sendable (Any) -> (() -> Any) 12 | 13 | public init( 14 | _ semaphore: S 15 | ) { 16 | self.base = semaphore 17 | 18 | signalImpl = { S.signal($0 as! S) } 19 | waitImpl = { S.wait($0 as! S) } 20 | } 21 | 22 | @discardableResult 23 | public func signal() -> Any { 24 | return signalImpl(base)() 25 | } 26 | 27 | @discardableResult 28 | public func wait() -> Any { 29 | return waitImpl(base)() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Merge/module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | @_exported import Diagnostics 6 | @_exported import Combine 7 | @_exported import Swallow 8 | @_exported import SwallowMacrosClient 9 | @_exported import SwiftDI 10 | 11 | public enum _module { 12 | 13 | } 14 | 15 | // MARK: - Deprecated 16 | 17 | @available(*, deprecated, renamed: "ObservableTaskFailure") 18 | public typealias TaskFailure = ObservableTaskFailure 19 | @available(*, deprecated, renamed: "ObservableTaskStatusType") 20 | public typealias TaskStatusType = ObservableTaskStatusType 21 | @available(*, deprecated, renamed: "ObservableTaskStatus") 22 | public typealias TaskStatus = ObservableTaskStatus 23 | @available(*, deprecated, renamed: "ObservableTaskStatusDescription") 24 | public typealias TaskStatusDescription = ObservableTaskStatusDescription 25 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Foundation/NSLocking++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Foundation 7 | 8 | extension NSLocking { 9 | /// Performs `body` with the lock held. 10 | public func _performWithLock( 11 | _ body: () throws -> Result 12 | ) rethrows -> Result { 13 | self.lock() 14 | defer { self.unlock() } 15 | return try body() 16 | } 17 | 18 | /// Given that the lock is held, **unlocks it**, performs `body`, 19 | /// then relocks it. 20 | /// 21 | /// Be very careful with your thread-safety analysis when using this function! 22 | public func _performWithoutLock( 23 | _ body: () throws -> Result 24 | ) rethrows -> Result { 25 | self.unlock() 26 | defer { self.lock() } 27 | return try body() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Dispatch/DispatchQoS.QoSClass++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Dispatch 6 | import Foundation 7 | import Swallow 8 | 9 | extension DispatchQoS.QoSClass { 10 | public static var current: Self { 11 | DispatchQoS.QoSClass(rawValue: qos_class_self()) ?? .unspecified 12 | } 13 | 14 | public init(qos: QualityOfService) { 15 | switch qos { 16 | case .userInteractive: 17 | self = .userInteractive 18 | case .userInitiated: 19 | self = .userInitiated 20 | case .utility: 21 | self = .utility 22 | case .background: 23 | self = .background 24 | case .default: 25 | self = .default 26 | @unknown default: 27 | self = .default 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/_ObservableTaskGroup+SwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import SwiftUI 6 | 7 | extension View { 8 | /// Supplies a task pipeline to a view subhierachy. 9 | public func _observableTaskGroup( 10 | _ value: T 11 | ) -> some View { 12 | environment(\._observableTaskGroup, value).environmentObject(value) 13 | } 14 | } 15 | 16 | // MARK: - Auxiliary 17 | 18 | extension EnvironmentValues { 19 | struct _ObservableTaskGroupKey: SwiftUI.EnvironmentKey { 20 | static let defaultValue: (any _ObservableTaskGroupType)? = nil 21 | } 22 | 23 | public var _observableTaskGroup: (any _ObservableTaskGroupType)? { 24 | get { 25 | self[_ObservableTaskGroupKey.self] 26 | } 27 | set { 28 | self[_ObservableTaskGroupKey.self] = newValue 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/_Concurrency/Clock++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 8 | extension Clock { 9 | @_disfavoredOverload 10 | public func measure( 11 | _ work: () throws -> T 12 | ) rethrows -> (result: T, duration: Duration) { 13 | var result: T! 14 | 15 | let duration = try measure { 16 | result = try work() 17 | } 18 | 19 | return (result, duration) 20 | } 21 | 22 | @_disfavoredOverload 23 | public func measure( 24 | _ work: () async throws -> T 25 | ) async rethrows -> (result: T, duration: Duration) { 26 | var result: T! 27 | 28 | let duration = try await measure { 29 | result = try await work() 30 | } 31 | 32 | return (result, duration) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/EmptyObservableTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Foundation 7 | 8 | public final class EmptyObservableTask: ObservableTask { 9 | public var status: ObservableTaskStatus { 10 | .idle 11 | } 12 | 13 | public var objectWillChange: AnyPublisher, Never> { 14 | Empty().eraseToAnyPublisher() 15 | } 16 | 17 | public var objectDidChange: AnyPublisher, Never> { 18 | Empty().eraseToAnyPublisher() 19 | } 20 | 21 | public init() { 22 | 23 | } 24 | 25 | public init() where Success == Void, Error == Never { 26 | 27 | } 28 | 29 | public func start() { 30 | 31 | } 32 | 33 | public func cancel() { 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/SinkPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | @frozen 9 | public struct SinkPublisher: Publisher { 10 | public typealias Output = P.Output 11 | public typealias Failure = P.Failure 12 | 13 | public let base = PassthroughSubject() 14 | public var subscription: AnyCancellable 15 | 16 | @inlinable 17 | public init(publisher: P) { 18 | subscription = publisher.subscribe(base) 19 | } 20 | 21 | @inlinable 22 | public func receive( 23 | subscriber: S 24 | ) where S.Input == Output, S.Failure == Failure { 25 | base.receive(subscriber: subscriber) 26 | } 27 | } 28 | 29 | extension Publisher { 30 | @inlinable 31 | public func sinkPublisher() -> SinkPublisher { 32 | SinkPublisher(publisher: self) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publishers.IndexedPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Publishers { 9 | public struct IndexedPublisher: Publisher where Upstream: Publisher { 10 | public struct Output { 11 | let index: Int 12 | let value: Upstream.Output 13 | } 14 | 15 | public typealias Failure = Upstream.Failure 16 | 17 | let index: Int 18 | let publisher: Upstream 19 | 20 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 21 | let cancellable = publisher.sink(receiveCompletion: subscriber.receive) { result in 22 | _ = subscriber.receive(.init(index: self.index, value: result)) 23 | } 24 | 25 | subscriber.receive(subscription: AnySubscription(cancellable)) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publisher.unwrap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | private enum UnwrapError: Error { 9 | case this 10 | } 11 | 12 | extension Publisher where Failure == Never { 13 | public func unwrap() -> Publishers.TryMap where Optional == Output { 14 | tryMap { value -> Wrapped in 15 | guard let value = value else { 16 | throw UnwrapError.this 17 | } 18 | 19 | return value 20 | } 21 | } 22 | } 23 | 24 | extension Publisher where Failure == Error { 25 | public func unwrap() -> Publishers.TryMap where Optional == Output { 26 | tryMap { value -> Wrapped in 27 | guard let value = value else { 28 | throw UnwrapError.this 29 | } 30 | 31 | return value 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/_opaque_ObservableObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | public protocol _opaque_ObservableObject: ObservableObject { 9 | var _opaque_objectWillChange: AnyObjectWillChangePublisher { get } 10 | 11 | func _opaque_objectWillChange_send() throws 12 | } 13 | 14 | // MARK: - Implementation 15 | 16 | extension ObservableObject { 17 | public var _opaque_objectWillChange: AnyObjectWillChangePublisher { 18 | AnyObjectWillChangePublisher(from: self) 19 | } 20 | } 21 | 22 | extension _opaque_ObservableObject where Self: ObservableObject { 23 | public func _opaque_objectWillChange_send() throws { 24 | try cast(objectWillChange, to: _opaque_VoidSender.self).send() 25 | } 26 | } 27 | 28 | // MARK: - Conformances 29 | 30 | #if canImport(CoreData) 31 | 32 | import CoreData 33 | 34 | extension NSManagedObject: _opaque_ObservableObject { 35 | 36 | } 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/Foundation/TaskResultPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | public struct TaskResultPublisher: SingleOutputPublisher { 8 | public typealias Output = TaskResult 9 | public typealias Failure = Never 10 | 11 | private let upstream: Upstream 12 | 13 | public init(upstream: Upstream) { 14 | self.upstream = upstream 15 | } 16 | 17 | public func receive( 18 | subscriber: S 19 | ) where S.Input == Output, S.Failure == Failure { 20 | if let result = TaskResult(upstream.status) { 21 | _ = subscriber.receive(result) 22 | 23 | subscriber.receive(completion: .finished) 24 | } else { 25 | upstream 26 | .objectDidChange 27 | .compactMap(TaskResult.init) 28 | .receive(subscriber: subscriber) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publisher.sink+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Publisher { 9 | /// Attaches an anonymous subscriber. 10 | public func sink() -> AnyCancellable { 11 | sink(receiveCompletion: { _ in }, receiveValue: { _ in }) 12 | } 13 | } 14 | 15 | extension SingleOutputPublisher { 16 | /// Attaches a subscriber with closure-based behavior. 17 | public func sinkResult( 18 | _ receiveValue: @escaping (Result) -> () 19 | ) -> AnyCancellable { 20 | sink( 21 | receiveCompletion: { completion in 22 | switch completion { 23 | case .finished: 24 | break 25 | case .failure(let error): 26 | receiveValue(.failure(error)) 27 | } 28 | }, 29 | receiveValue: { value in 30 | receiveValue(.success(value)) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/Foundation/ObservableTaskBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Foundation 7 | import Swift 8 | 9 | /// A base class to subclass when building observable tasks. 10 | open class ObservableTaskBase: ObservableTask { 11 | public typealias Status = ObservableTaskStatus 12 | 13 | let statusValueSubject = CurrentValueSubject(.idle) 14 | 15 | public var objectWillChange: AnyPublisher { 16 | statusValueSubject.receiveOnMainThread().eraseToAnyPublisher() 17 | } 18 | 19 | public var objectDidChange: AnyPublisher { 20 | statusValueSubject.eraseToAnyPublisher() 21 | } 22 | 23 | public var status: Status { 24 | statusValueSubject.value 25 | } 26 | 27 | public init() { 28 | 29 | } 30 | 31 | public func start() { 32 | 33 | } 34 | 35 | public func cancel() { 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/_CancellablesProviding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import ObjectiveC 7 | 8 | /// A type that provides a `Cancellables` instance. 9 | public protocol _CancellablesProviding { 10 | var cancellables: Cancellables { get } 11 | } 12 | 13 | // MARK: - Implementation 14 | 15 | private var cancellables_objcAssociationKey: UInt = 0 16 | 17 | extension _CancellablesProviding where Self: AnyObject { 18 | public var cancellables: Cancellables { 19 | objc_sync_enter(self) 20 | 21 | defer { 22 | objc_sync_exit(self) 23 | } 24 | 25 | if let result = objc_getAssociatedObject(self, &cancellables_objcAssociationKey) as? Cancellables { 26 | return result 27 | } else { 28 | let result = Cancellables() 29 | 30 | objc_setAssociatedObject(self, &cancellables_objcAssociationKey, result, .OBJC_ASSOCIATION_RETAIN) 31 | 32 | return result 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftDI/Intramodular/Core/TaskDependencies.LookupRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | private import ObjectiveC 6 | import Diagnostics 7 | import Swallow 8 | 9 | extension TaskDependencies { 10 | protocol _opaque_LookupRequest { 11 | func _opaque_resolve(from dependencies: TaskDependencies) throws -> Any? 12 | } 13 | 14 | public enum LookupRequest: _opaque_LookupRequest { 15 | case unkeyed(Value.Type) 16 | case keyed(KeyPath) 17 | 18 | func _opaque_resolve( 19 | from dependencies: TaskDependencies 20 | ) throws -> Any? { 21 | try dependencies.resolve(self) 22 | } 23 | } 24 | 25 | func resolve( 26 | _ request: LookupRequest 27 | ) throws -> T? { 28 | switch request { 29 | case .unkeyed(let type): 30 | return try unkeyedValues.firstAndOnly(ofType: type) 31 | case .keyed(let key): 32 | return self[key] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/AsyncStreamTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import Testing 4 | @testable import Merge 5 | 6 | @Suite 7 | struct AsyncStreamTests { 8 | @Test("AsyncStream cancellation propagates to publisher") 9 | func testCancellation() async throws { 10 | let publisher = Timer.publish(every: 0.1, on: .main, in: .common) 11 | .autoconnect() 12 | 13 | try await confirmation { confirm in 14 | var count: Int = 0 15 | 16 | let cancellablePublisher = publisher.handleCancel { 17 | print("✅ Publisher was canceled") 18 | confirm() 19 | } 20 | 21 | let stream = cancellablePublisher.toAsyncStream() 22 | 23 | let task = Task { 24 | for await _ in stream { 25 | print("value") 26 | count += 1 27 | } 28 | } 29 | 30 | try await Task.sleep(for: .milliseconds(300)) 31 | 32 | task.cancel() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publishers.Catch+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | extension Publisher { 9 | /// Discards all errors in the stream. 10 | @_transparent 11 | public func discardError() -> Publishers.Catch, Never>> { 12 | self.catch({ _ in Combine.Empty().setFailureType(to: Never.self) }) 13 | } 14 | 15 | /// Handles errors from an upstream publisher by stopping the execution of the program. 16 | @_transparent 17 | public func stopExecutionOnError() -> Publishers.Catch, Never>> { 18 | self.catch { error -> Publishers.SetFailureType, Never> in 19 | fatalError(error) 20 | } 21 | } 22 | 23 | @_transparent 24 | public func catchAndMapTo( 25 | _ output: Output 26 | ) -> Publishers.Catch> { 27 | self.catch({ _ in Just(output) }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Vatsal Manot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publisher.join.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Publisher { 9 | public func join(_ publishers: S) -> AnyPublisher where P.Output == Output, P.Failure == Failure, S.Element == P { 10 | publishers.reduce(eraseToAnyPublisher()) { result, next in 11 | result.flatMap { output in 12 | Publishers.Concatenate( 13 | prefix: Just(output).setFailureType(to: Failure.self), 14 | suffix: next 15 | ) 16 | } 17 | .eraseToAnyPublisher() 18 | } 19 | .eraseToAnyPublisher() 20 | } 21 | } 22 | 23 | extension Collection where Element: Publisher { 24 | public func join() -> AnyPublisher { 25 | if let first = first { 26 | return first.join(dropFirst()).eraseToAnyPublisher() 27 | } else { 28 | return Empty().setFailureType(to: Element.Failure.self).eraseToAnyPublisher() 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/_AnyObservableTaskGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | public protocol _ObservableTaskGroupType: _CancellablesProviding, ObservableObject { 9 | typealias TaskHistory = [ObservableTaskStatusDescription] 10 | 11 | associatedtype Key 12 | 13 | @MainActor 14 | subscript(customIdentifier identifier: Key) -> IdentifierIndexingArrayOf { get } 15 | 16 | func cancelAll() 17 | 18 | @MainActor 19 | func _opaque_lastStatus( 20 | forCustomTaskIdentifier identifier: AnyHashable 21 | ) throws -> ObservableTaskStatusDescription? 22 | } 23 | 24 | public class _AnyObservableTaskGroup: ObservableObject { 25 | 26 | } 27 | 28 | // MARK: - Internal 29 | 30 | extension _ObservableTaskGroup { 31 | @MainActor 32 | public func _opaque_lastStatus( 33 | forCustomTaskIdentifier identifier: AnyHashable 34 | ) throws -> ObservableTaskStatusDescription? { 35 | self.lastStatus(forCustomTaskIdentifier: try cast(identifier.base, to: Key.self)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Notifications/NotificationPublishing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | public protocol NotificationPublishing { 9 | associatedtype NotificationPublisherType: Publisher where NotificationPublisherType.Failure == Never 10 | 11 | var notificationPublisher: NotificationPublisherType { get } 12 | } 13 | 14 | extension NotificationPublishing where Self: ObservableObject { 15 | public func onNotification( 16 | _ x: NotificationPublisherType.Output, 17 | perform action: @escaping () -> Void 18 | ) where NotificationPublisherType.Output: Equatable { 19 | _onReceiveOfValueEmittedBy(notificationPublisher.filter({ $0 == x })) { _ in 20 | action() 21 | } 22 | } 23 | 24 | public func onNotification( 25 | _ type: NotificationPublisherType.Output.TypeDiscriminator, 26 | perform action: @escaping () -> Void 27 | ) where NotificationPublisherType.Output: TypeDiscriminable { 28 | _onReceiveOfValueEmittedBy(notificationPublisher.filter(type)) { _ in 29 | action() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/Status/TaskOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | /// The output of a task. 9 | public enum TaskOutput { 10 | case started 11 | case success(Success) 12 | } 13 | 14 | extension TaskOutput { 15 | public var value: Success? { 16 | ObservableTaskStatus(self).successValue 17 | } 18 | 19 | public var isTerminal: Bool { 20 | switch self { 21 | case .success: 22 | return true 23 | default: 24 | return false 25 | } 26 | } 27 | 28 | public func map(_ transform: (Success) -> T) -> TaskOutput { 29 | switch self { 30 | case .started: 31 | return .started 32 | case .success(let success): 33 | return .success(transform(success)) 34 | } 35 | } 36 | } 37 | 38 | // MARK: - Conformances 39 | 40 | extension TaskOutput: Equatable where Success: Equatable { 41 | 42 | } 43 | 44 | extension TaskOutput: Hashable where Success: Hashable { 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Subscriptions.AddCancellable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Subscriptions { 9 | public final class AddCancellable: Subscription { 10 | private let mutex = OSUnfairLock() 11 | private let base: Base 12 | private var cancellable: Cancellable? 13 | 14 | public init(base: Base, cancellable: Cancellable) { 15 | self.base = base 16 | self.cancellable = cancellable 17 | } 18 | 19 | public func request(_ demand: Subscribers.Demand) { 20 | base.request(demand) 21 | } 22 | 23 | public func cancel() { 24 | mutex.withCriticalScope { 25 | base.cancel() 26 | 27 | cancellable?.cancel() 28 | cancellable = nil 29 | } 30 | } 31 | } 32 | } 33 | 34 | // MARK: - API 35 | 36 | extension Subscription { 37 | public func add(_ cancellable: Cancellable) -> Subscription { 38 | Subscriptions.AddCancellable(base: self, cancellable: cancellable) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Combine/AnyPublisher++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension AnyPublisher { 9 | public static func result(_ result: Result) -> Self { 10 | switch result { 11 | case .failure(let failure): 12 | return Fail(error: failure).eraseToAnyPublisher() 13 | case .success(let output): 14 | return Just(output) 15 | .setFailureType(to: Failure.self) 16 | .eraseToAnyPublisher() 17 | 18 | } 19 | } 20 | 21 | public static func just(_ output: Output) -> Self { 22 | Just(output) 23 | .setFailureType(to: Failure.self) 24 | .eraseToAnyPublisher() 25 | } 26 | 27 | public static func failure(_ failure: Failure) -> Self { 28 | Result.Publisher(failure).eraseToAnyPublisher() 29 | } 30 | 31 | public static func empty(completeImmediately: Bool = true) -> Self { 32 | Empty(completeImmediately: completeImmediately) 33 | .setFailureType(to: Failure.self) 34 | .eraseToAnyPublisher() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Future+DispatchQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Dispatch 7 | import Swift 8 | 9 | extension Future { 10 | // Schedules a block asynchronously for execution. 11 | public static func async( 12 | qos: DispatchQoS.QoSClass, 13 | execute work: @escaping () -> Output 14 | ) -> Future where Failure == Never { 15 | .init { attemptToFulfill in 16 | DispatchQueue.global(qos: qos).async { 17 | attemptToFulfill(.success(work())) 18 | } 19 | } 20 | } 21 | 22 | // Schedules a block asynchronously for execution. 23 | public static func async( 24 | qos: DispatchQoS.QoSClass, 25 | execute work: @escaping () throws -> Output 26 | ) -> Future where Failure == Error { 27 | .init { attemptToFulfill in 28 | DispatchQueue.global(qos: qos).async { 29 | do { 30 | attemptToFulfill(.success(try work())) 31 | } catch { 32 | attemptToFulfill(.failure(error)) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/ObservableTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Diagnostics 7 | import Foundation 8 | import Swallow 9 | 10 | /// An observable task is a token of activity with status-reporting. 11 | public protocol ObservableTask: Cancellable, Identifiable, ObjectDidChangeObservableObject where ObjectDidChangePublisher.Output == ObservableTaskStatus { 12 | associatedtype Success 13 | associatedtype Error: Swift.Error 14 | 15 | /// The status of this task. 16 | var status: ObservableTaskStatus { get } 17 | 18 | /// Start the task. 19 | func start() 20 | 21 | /// Cancel the task. 22 | func cancel() 23 | } 24 | 25 | extension ObservableTask { 26 | public var statusDescription: ObservableTaskStatusDescription { 27 | .init(status) 28 | } 29 | } 30 | 31 | // MARK: - Implementation 32 | 33 | extension Subscription where Self: ObservableTask { 34 | public func request(_ demand: Subscribers.Demand) { 35 | guard demand != .none, statusDescription == .idle else { 36 | return 37 | } 38 | 39 | start() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/SwiftDI/Intramodular/Core/TaskDependencyKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | /// A hack so that `Domain` can be inferred to be `Dependencies`. 7 | public protocol _TaskDependencyKey { 8 | typealias Domain = TaskDependencies 9 | } 10 | 11 | /// A key for accessing dependencies in the local task context. 12 | public protocol TaskDependencyKey: _TaskDependencyKey, HeterogeneousDictionaryKey where Domain == TaskDependencies { 13 | @_spi(Internal) 14 | static var attributes: Set<_TaskDependencyAttribute> { get } 15 | 16 | static var defaultValue: Value { get } 17 | } 18 | 19 | 20 | // MARK: - Implementation 21 | 22 | extension TaskDependencyKey { 23 | public static var attributes: Set<_TaskDependencyAttribute> { 24 | [] 25 | } 26 | } 27 | 28 | // MARK: - Auxiliary 29 | 30 | public enum _TaskDependencyAttribute { 31 | case unstashable 32 | } 33 | 34 | // MARK: - Conformees 35 | 36 | public struct _OptionalTaskDependencyKey: TaskDependencyKey { 37 | public typealias Domain = TaskDependencies 38 | public typealias Value = T? 39 | 40 | public static var defaultValue: Value { 41 | nil 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Protocol Conformances/Dispatch/Dispatch+Mutex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Dispatch 6 | import Swallow 7 | 8 | public struct DispatchMutexDevice: ScopedReadWriteMutexProtocol, @unchecked Sendable { 9 | @MutexProtected 10 | private var queue: DispatchQueue 11 | 12 | public init(label: String? = nil, target: DispatchQueue? = nil) { 13 | self._queue = .init( 14 | wrappedValue: DispatchQueue( 15 | label: label ?? "com.vmanot.Merge.DispatchMutexDevice", 16 | attributes: [.concurrent], 17 | target: target 18 | ) 19 | ) 20 | } 21 | 22 | public func withCriticalScopeForReading(_ f: (() throws -> T)) rethrows -> T { 23 | return try queue.sync { 24 | try f() 25 | } 26 | } 27 | 28 | public func withCriticalScopeForWriting(_ f: (() throws -> T)) rethrows -> T { 29 | return try queue.sync(flags: .barrier) { 30 | try f() 31 | } 32 | } 33 | } 34 | 35 | // MARK: - Conformances 36 | 37 | extension DispatchMutexDevice: Initiable { 38 | public init() { 39 | self.init(target: nil) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Dispatch/DispatchTimeInterval++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Dispatch 6 | import Foundation 7 | import Swallow 8 | 9 | extension DispatchTimeInterval { 10 | /// Converts the value to a `TimeInterval`. 11 | /// 12 | /// Throws if the conversion fails (for e.g. if attempting to convert `.never`). 13 | public func toTimeInterval() throws -> TimeInterval { 14 | enum ConversionError: Error { 15 | case failedToConvertNever 16 | case unknownCase 17 | } 18 | 19 | var result: Double 20 | 21 | switch self { 22 | case .seconds(let value): 23 | result = Double(value) 24 | case .milliseconds(let value): 25 | result = Double(value) * 0.001 26 | case .microseconds(let value): 27 | result = Double(value) * 0.000001 28 | case .nanoseconds(let value): 29 | result = Double(value) * 0.000000001 30 | case .never: 31 | throw ConversionError.failedToConvertNever 32 | @unknown default: 33 | throw ConversionError.unknownCase 34 | } 35 | 36 | return result 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CommandLineToolSupport/Intramodular/Builtins/_CommandLineToolEnvironmentVariable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swallow 7 | 8 | public protocol _CommandLineToolEnvironmentVariableProtocol: PropertyWrapper where WrappedValue: CLT.EnvironmentVariableValue { 9 | /// The name of the environment variable as it will be passed in the actual command being invoked. 10 | /// 11 | /// For e.g. `TARGET_BUILD_DIR` for `xcodebuild`. 12 | var name: String { get } 13 | } 14 | 15 | @propertyWrapper 16 | public struct _CommandLineToolEnvironmentVariable: _CommandLineToolEnvironmentVariableProtocol { 17 | var _wrappedValue: WrappedValue 18 | 19 | public let name: String 20 | 21 | public var wrappedValue: WrappedValue { 22 | get { 23 | _wrappedValue 24 | } set { 25 | _wrappedValue = newValue 26 | } 27 | } 28 | 29 | public init(wrappedValue: WrappedValue, name: String) { 30 | self._wrappedValue = wrappedValue 31 | self.name = name 32 | } 33 | } 34 | 35 | extension CommandLineTool { 36 | public typealias EnvironmentVariable = _CommandLineToolEnvironmentVariable 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Protocol Conformances/Swallow/Swallow+Publisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | extension Either: Combine.Publisher 9 | where 10 | LeftValue: Publisher, 11 | RightValue: Publisher, 12 | LeftValue.Output == RightValue.Output, 13 | LeftValue.Failure == RightValue.Failure 14 | { 15 | public typealias Output = LeftValue.Output 16 | public typealias Failure = LeftValue.Failure 17 | 18 | public func receive( 19 | subscriber: S 20 | ) where S.Failure == Failure, S.Input == Output { 21 | switch self { 22 | case .left(let publisher): 23 | publisher.receive(subscriber: subscriber) 24 | case .right(let publisher): 25 | publisher.receive(subscriber: subscriber) 26 | } 27 | } 28 | 29 | public init(@PublisherBuilder _ createPublisher: () -> Either) { 30 | self = createPublisher() 31 | } 32 | } 33 | 34 | extension Either: Merge.SingleOutputPublisher 35 | where 36 | LeftValue: SingleOutputPublisher, 37 | RightValue: SingleOutputPublisher, 38 | LeftValue.Output == RightValue.Output, 39 | LeftValue.Failure == RightValue.Failure 40 | { 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/PublisherQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | public struct PublisherQueue: Publisher { 9 | public typealias Output = Upstream.Output 10 | public typealias Failure = Upstream.Failure 11 | 12 | private let cancellables = Cancellables() 13 | private let scheduler: Context 14 | private let output = PassthroughSubject() 15 | 16 | public init(scheduler: Context) { 17 | self.scheduler = scheduler 18 | } 19 | 20 | public func receive(subscriber: S) where S.Input == Output, S.Failure == Failure { 21 | output.subscribe(subscriber) 22 | } 23 | 24 | public func send(_ publisher: Upstream) { 25 | scheduler.schedule { 26 | publisher.receive(on: scheduler).sinkResult( 27 | in: cancellables, 28 | receiveValue: { 29 | switch $0 { 30 | case .success(let value): 31 | output.send(value) 32 | case .failure(let error): 33 | output.send(completion: .failure(error)) 34 | } 35 | }) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Dispatch/DispatchQoS+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Dispatch 6 | import Swallow 7 | 8 | extension DispatchQoS.QoSClass: Swift.CaseIterable { 9 | public static let allCases: [DispatchQoS.QoSClass] = [ 10 | .background, 11 | .utility, 12 | .`default`, 13 | .userInitiated, 14 | .userInteractive, 15 | .unspecified 16 | ] 17 | } 18 | 19 | extension DispatchQoS: Swift.Comparable { 20 | public static func < (lhs: DispatchQoS, rhs: DispatchQoS) -> Bool { 21 | return lhs.qosClass < rhs.qosClass 22 | } 23 | } 24 | 25 | extension DispatchQoS.QoSClass: Swift.Comparable { 26 | private var sortValue: UInt32 { 27 | #if !os(Linux) 28 | return rawValue.rawValue 29 | #else 30 | switch self { 31 | case .background: return 0x09 32 | // case .maintenance: return 0x05 33 | case .utility: return 0x11 34 | case .default: return 0x15 35 | case .userInitiated: return 0x19 36 | case .userInteractive: return 0x21 37 | case .unspecified: return 0x00 38 | } 39 | #endif 40 | } 41 | 42 | public static func < (lhs: DispatchQoS.QoSClass, rhs: DispatchQoS.QoSClass) -> Bool { 43 | return lhs.sortValue < rhs.sortValue 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/PublisherBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | import SwiftUI 8 | 9 | #if swift(<5.4) 10 | @_functionBuilder 11 | public struct PublisherBuilder { 12 | 13 | } 14 | #else 15 | @resultBuilder 16 | public struct PublisherBuilder { 17 | 18 | } 19 | #endif 20 | 21 | extension PublisherBuilder { 22 | public static func buildBlock() -> Combine.Empty { 23 | .init() 24 | } 25 | 26 | public static func buildBlock(_ publisher: P) -> P { 27 | publisher 28 | } 29 | 30 | public static func buildIf(_ publisher: P?) -> Either> { 31 | if let publisher = publisher { 32 | return .left(publisher) 33 | } else { 34 | return .right(Empty()) 35 | } 36 | } 37 | 38 | public static func buildEither( 39 | first: TruePublisher 40 | ) -> Either { 41 | .left(first) 42 | } 43 | 44 | public static func buildEither( 45 | second: FalsePublisher 46 | ) -> Either { 47 | .right(second) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/SwiftDI/Intramodular/SwiftUI/TaskDependencies+SwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | import SwiftUI 7 | 8 | extension EnvironmentValues { 9 | fileprivate struct _TaskDependenciesKey: EnvironmentKey { 10 | static let defaultValue = TaskDependencies() 11 | } 12 | 13 | var _dependencies: TaskDependencies { 14 | get { 15 | self[_TaskDependenciesKey.self] 16 | } set { 17 | self[_TaskDependenciesKey.self] = newValue 18 | } 19 | } 20 | } 21 | 22 | extension View { 23 | public func dependencies( 24 | _ dependencies: TaskDependencies 25 | ) -> some View { 26 | transformEnvironment(\._dependencies) { 27 | $0.mergeInPlace(with: dependencies) 28 | } 29 | } 30 | 31 | public func dependency( 32 | _ key: WritableKeyPath, 33 | _ value: V 34 | ) -> some View { 35 | transformEnvironment(\._dependencies) { 36 | $0[key] = value 37 | } 38 | } 39 | } 40 | 41 | public func withTaskDependencies( 42 | from subject: Subject, 43 | @ViewBuilder content: () -> Content 44 | ) -> some View { 45 | var result: Content! 46 | 47 | withTaskDependencies(from: subject) { 48 | result = content() 49 | } 50 | 51 | return result 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publishers.MapSubscriber.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Publishers { 9 | public struct MapSubscriber: Publisher where Subscriber.Input == Upstream.Output, Subscriber.Failure == Upstream.Failure { 10 | public typealias Output = Upstream.Output 11 | public typealias Failure = Upstream.Failure 12 | 13 | private let upstream: Upstream 14 | private let transform: (AnySubscriber) -> Subscriber 15 | 16 | public init( 17 | upstream: Upstream, 18 | transform: @escaping (AnySubscriber) -> Subscriber 19 | ) { 20 | self.upstream = upstream 21 | self.transform = transform 22 | } 23 | 24 | public func receive( 25 | subscriber: S 26 | ) where S.Input == Output, S.Failure == Failure { 27 | upstream.receive(subscriber: transform(.init(subscriber))) 28 | } 29 | } 30 | } 31 | 32 | extension Publisher { 33 | public func mapSubscriber( 34 | _ transform: @escaping (AnySubscriber) -> S 35 | ) -> Publishers.MapSubscriber { 36 | .init(upstream: self, transform: transform) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publishers.FlatMapLatest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Publishers { 9 | public struct FlatMapLatest: Publisher where NewPublisher.Failure == Upstream.Failure { 10 | public typealias Output = NewPublisher.Output 11 | public typealias Failure = Upstream.Failure 12 | 13 | private let upstream: Upstream 14 | private let transform: (Upstream.Output) -> NewPublisher 15 | 16 | public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> NewPublisher) { 17 | self.upstream = upstream 18 | self.transform = transform 19 | } 20 | 21 | public func receive(subscriber: S) where S.Input == NewPublisher.Output, S.Failure == Upstream.Failure { 22 | upstream 23 | .map(transform) 24 | .switchToLatest() 25 | .receive(subscriber: subscriber) 26 | } 27 | } 28 | } 29 | 30 | // MARK: - Helpers 31 | 32 | extension Publisher { 33 | public func flatMapLatest( 34 | _ transform: @escaping (Output) -> NewPublisher 35 | ) -> Publishers.FlatMapLatest 36 | where NewPublisher.Failure == Failure { 37 | return .init(upstream: self, transform: transform) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/SingleAssignmentAnyCancellable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Dispatch 7 | import Swift 8 | 9 | public final class SingleAssignmentAnyCancellable: Cancellable, @unchecked Sendable { 10 | private let lock = OSUnfairLock() 11 | 12 | private var _isCanceled: Bool? 13 | private var base: AnyCancellable? 14 | 15 | public var isCanceled: Bool { 16 | lock.withCriticalScope { 17 | if let _isCanceled { 18 | return _isCanceled 19 | } else { 20 | return base == nil 21 | } 22 | } 23 | } 24 | 25 | public init() { 26 | 27 | } 28 | 29 | public func set(_ base: C) { 30 | lock.withCriticalScope { 31 | guard !(_isCanceled == true) else { 32 | base.cancel() 33 | 34 | return 35 | } 36 | 37 | self.base = .init(base) 38 | } 39 | } 40 | 41 | public func cancel() { 42 | lock.withCriticalScope { 43 | guard !(_isCanceled == true) else { 44 | assertionFailure() 45 | 46 | return 47 | } 48 | 49 | self.base?.cancel() 50 | self.base = nil 51 | 52 | _isCanceled = true 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Dispatch/DispatchSource++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | #if os(macOS) 6 | 7 | import Combine 8 | import Dispatch 9 | import Foundation 10 | import Swift 11 | 12 | extension DispatchSource { 13 | static func makeReadTextSource( 14 | pipe: Pipe, 15 | queue: DispatchQueue, 16 | sink: S, 17 | encoding: String.Encoding = .utf8 18 | ) -> DispatchSourceRead where S.Output == String { 19 | let pipe = Pipe() 20 | let fileDescriptor = pipe.fileHandleForReading.fileDescriptor 21 | let readSource = DispatchSource.makeReadSource(fileDescriptor: fileDescriptor, queue: queue) 22 | 23 | readSource.setEventHandler { [weak readSource = readSource] in 24 | guard let data = readSource?.data else { 25 | return 26 | } 27 | 28 | let estimatedBytesAvailableToRead = Int(data) 29 | 30 | var buffer = [UInt8](repeating: 0, count: estimatedBytesAvailableToRead) 31 | let bytesRead = read(fileDescriptor, &buffer, estimatedBytesAvailableToRead) 32 | 33 | guard bytesRead > 0, let availableString = String(bytes: buffer, encoding: encoding) else { 34 | return 35 | } 36 | 37 | sink.send(availableString) 38 | } 39 | 40 | return readSource 41 | } 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publishers.Map+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Publisher { 9 | /// Transforms all elements from an upstream publisher into an empty publisher. 10 | public func mapToEmpty(completeImmediately: Bool = true) -> Publishers.FlatMap, Self> { 11 | flatMap({ _ in Empty(completeImmediately: completeImmediately) }) 12 | } 13 | 14 | /// Transforms all elements from an upstream publisher into an empty publisher. 15 | public func mapToEmpty( 16 | completeImmediately: Bool = true, 17 | outputType: Output.Type = Output.self, 18 | failureType: Failure.Type = Failure.self 19 | ) -> Publishers.FlatMap, Self> { 20 | flatMap({ _ in Empty(completeImmediately: completeImmediately) }) 21 | } 22 | 23 | /// Maps all elements from an upstream publisher to a single value. 24 | public func mapTo(_ value: T) -> Publishers.Map { 25 | map({ _ in value }) 26 | } 27 | 28 | /// Maps all elements from an upstream publisher to a single value. 29 | public func mapTo(_ value: @escaping () -> T) -> Publishers.Map { 30 | map({ _ in value() }) 31 | } 32 | 33 | public func reduceAndMapTo(_ value: T) -> Publishers.Map, T> { 34 | reduce((), { _, _ in () }).mapTo(value) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Subscribers.MapSubscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Subscribers { 9 | /// A subscriber that transforms the received subscription with a provided closure. 10 | public final class MapSubscription: Subscriber { 11 | public typealias Input = Base.Input 12 | public typealias Failure = Base.Failure 13 | 14 | private let base: Base 15 | private let transform: (Subscription) -> Subscription 16 | 17 | public init(base: Base, transform: @escaping (Subscription) -> Subscription) { 18 | self.base = base 19 | self.transform = transform 20 | } 21 | 22 | public func receive(subscription: Subscription) { 23 | base.receive(subscription: transform(subscription)) 24 | } 25 | 26 | public func receive(_ input: Input) -> Subscribers.Demand { 27 | base.receive(input) 28 | } 29 | 30 | public func receive(completion: Subscribers.Completion) { 31 | base.receive(completion: completion) 32 | } 33 | } 34 | } 35 | 36 | // MARK: - API 37 | 38 | extension Subscriber { 39 | public func mapSubscription( 40 | _ transform: @escaping (Subscription) -> Subscription 41 | ) -> Subscribers.MapSubscription { 42 | .init(base: self, transform: transform) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publishers.ConcatenateMany.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Publishers { 9 | /// A publisher created by applying the concatenate function to many upstream publishers. 10 | /// 11 | /// Emits all of one publisher's elements before those from the next publisher. 12 | public struct ConcatenateMany: Publisher where Upstream: Publisher { 13 | public typealias Output = Upstream.Output 14 | public typealias Failure = Upstream.Failure 15 | 16 | public let publishers: [Upstream] 17 | 18 | public init(_ publishers: [Upstream]) { 19 | self.publishers = publishers 20 | } 21 | 22 | public init(_ upstream: Upstream...) { 23 | self.init(upstream) 24 | } 25 | 26 | public init(_ upstream: S) where Upstream == S.Element, S: Swift.Sequence { 27 | publishers = Array(upstream) 28 | } 29 | 30 | public func receive(subscriber: S) where ConcatenateMany.Failure == S.Failure, ConcatenateMany.Output == S.Input { 31 | let initial = AnyPublisher.empty() 32 | 33 | publishers.reduce(initial) { 34 | Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 35 | } 36 | .receive(subscriber: subscriber) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/_MaybeAsyncProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | /// A type that is **maybe** asynchronous. 8 | /// 9 | /// Utility protocol for things that might require preheating/one-off resource resolution and are then synchronous to use. 10 | public protocol _MaybeAsyncProtocol { 11 | /// Whether this instance is known to be asynchronous. 12 | var _isKnownAsync: Bool? { get } 13 | 14 | func _resolveToNonAsync() async throws -> Self 15 | } 16 | 17 | // MARK: - Implementation 18 | 19 | extension _MaybeAsyncProtocol { 20 | public var _isKnownAsync: Bool? { 21 | nil 22 | } 23 | 24 | public func _resolveToNonAsync() async throws -> Self { 25 | throw Never.Reason.unimplemented 26 | } 27 | } 28 | 29 | // MARK: - Supplementary 30 | 31 | @discardableResult 32 | public func _resolveMaybeAsync(_ x: T) async throws -> T { 33 | do { 34 | if let x = x as? _MaybeAsyncProtocol { 35 | return try await x._resolveToNonAsync() as! T 36 | } else { 37 | return x 38 | } 39 | } catch { 40 | switch error { 41 | case _MaybeAsyncProtocolError.noResolutionImplementation: 42 | return x 43 | default: 44 | throw error 45 | } 46 | } 47 | } 48 | 49 | // MARK: - Error Handling 50 | 51 | public enum _MaybeAsyncProtocolError: Equatable, Error { 52 | case noResolutionImplementation 53 | case needsResolving 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/_Concurrency/Task.firstResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Dispatch 7 | import Swift 8 | 9 | extension Task { 10 | public static func firstResult( 11 | from tasks: [(@Sendable () async throws -> R)] 12 | ) async throws -> R? { 13 | return try await withThrowingTaskGroup(of: R.self) { group in 14 | for task in tasks { 15 | group.addTask { 16 | try await task() 17 | } 18 | } 19 | // First finished child task wins, cancel the other task. 20 | let result = try await group.next() 21 | 22 | group.cancelAll() 23 | 24 | return result 25 | } 26 | } 27 | 28 | public static func firstResult( 29 | from tasks: [Task] 30 | ) async throws -> R? { 31 | return try await withThrowingTaskGroup(of: R.self) { group in 32 | for task in tasks { 33 | group.addTask { 34 | try await withTaskCancellationHandler { 35 | try await task.value 36 | } onCancel: { 37 | task.cancel() 38 | } 39 | } 40 | } 41 | 42 | let result = try await group.next() 43 | 44 | group.cancelAll() 45 | 46 | return result 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/CommandLineToolSupport/Intramodular/Builtins/CLT.EnvironmentVariable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | extension CLT { 9 | public protocol _EnvironmentVariableProtocol { 10 | associatedtype Value: EnvironmentVariableValue 11 | 12 | var name: String { get } 13 | var defaultValue: Value { get } 14 | } 15 | 16 | public struct EnvironmentVariable: _EnvironmentVariableProtocol { 17 | public let name: String 18 | public let defaultValue: Value 19 | 20 | public init(name: String, defaultValue: Value) { 21 | self.name = name 22 | self.defaultValue = defaultValue 23 | } 24 | } 25 | } 26 | 27 | extension CLT.EnvironmentVariable { 28 | public init(name: String) where Value: ExpressibleByNilLiteral { 29 | self.init(name: name, defaultValue: nil) 30 | } 31 | } 32 | 33 | @available(macOS 11.0, *) 34 | @available(iOS, unavailable) 35 | @available(macCatalyst, unavailable) 36 | @available(tvOS, unavailable) 37 | @available(watchOS, unavailable) 38 | extension CommandLineTool { 39 | public func setEnvironmentVariable( 40 | _ keyPath: KeyPath, 41 | _ value: Variable.Value 42 | ) { 43 | let variable = Self.EnvironmentVariables.self[keyPath: keyPath] 44 | 45 | self.environmentVariables[variable.name] = value 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/ThrowingTaskQueueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | @testable import Merge 6 | 7 | import Swallow 8 | import XCTest 9 | 10 | final class ThrowingTaskQueueTests: XCTestCase { 11 | func testReentrancy() async throws { 12 | let queue = TaskQueue() 13 | 14 | queue.addTask { 15 | await queue.perform { 16 | 0 17 | } 18 | } 19 | 20 | _ = await queue.perform { 21 | assert(queue.isActive) 22 | 23 | await queue.perform { 24 | 0 25 | } 26 | } 27 | 28 | await queue.waitForAll() 29 | } 30 | 31 | func testThrowingReentrancy() async throws { 32 | let queue = ThrowingTaskQueue() 33 | 34 | queue.addTask { 35 | try await queue.perform { 36 | 0 37 | } 38 | } 39 | 40 | _ = try await queue.perform { 41 | try await queue.perform { 42 | 0 43 | } 44 | } 45 | 46 | try await queue.waitForAll() 47 | } 48 | 49 | func testComplexReentrancy() async throws { 50 | let queue = ThrowingTaskQueue() 51 | let queue2 = ThrowingTaskQueue() 52 | 53 | queue.addTask { 54 | try await queue2.perform { 55 | try await queue.perform { 56 | 0 57 | } 58 | } 59 | } 60 | 61 | try await queue.waitForAll() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/ShellScripting/Intramodular/PreferredUNIXShell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | #if os(macOS) 6 | 7 | import Foundation 8 | import Swift 9 | 10 | /// Enum to represent different shell environments or direct execution. 11 | public enum PreferredUNIXShell { 12 | public enum Name: String, Codable, Hashable, Sendable { 13 | case sh 14 | case bash 15 | case zsh 16 | case unspecified 17 | } 18 | } 19 | 20 | extension PreferredUNIXShell.Name { 21 | /// Returns the shell executable path and initial arguments based on the environment. 22 | func deriveExecutableURLAndArguments( 23 | fromCommand command: String 24 | ) -> (executableURL: URL, arguments: [String]) { 25 | switch self { 26 | case .sh: 27 | return (URL(fileURLWithPath: "/bin/sh"), ["-l", "-c", command]) 28 | case .bash: 29 | return (URL(fileURLWithPath: "/bin/bash"), ["-l", "-c", command]) 30 | case .zsh: 31 | return (URL(fileURLWithPath: "/bin/zsh"), ["-l", "-c", command]) 32 | case .unspecified: 33 | fatalError() 34 | } 35 | } 36 | } 37 | 38 | extension Optional where Wrapped == PreferredUNIXShell.Name { 39 | func deriveExecutableURLAndArguments( 40 | fromCommand command: String 41 | ) -> (executableURL: URL, arguments: [String]) { 42 | guard let shell = self else { 43 | return (URL(fileURLWithPath: command), []) 44 | } 45 | 46 | return shell.deriveExecutableURLAndArguments(fromCommand: command) 47 | } 48 | } 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/Foundation/ObservableTask.concatenate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | extension ObservableTask where Success == Void, Error == Swift.Error { 8 | public func concatenate( 9 | with other: Self 10 | ) -> AnyTask { 11 | PassthroughTask { (task: PassthroughTask) in 12 | Publishers.Concatenate(prefix: self.successPublisher, suffix: other.successPublisher) 13 | .reduceAndMapTo(()) 14 | .sinkResult { result in 15 | switch result { 16 | case .success(let value): 17 | task.succeed(with: value) 18 | case .failure(let error): 19 | task.fail(with: error) 20 | } 21 | } 22 | } 23 | .eraseToAnyTask() 24 | } 25 | 26 | public func concatenate(with elements: [Self]) -> AnyTask { 27 | PassthroughTask { (task: PassthroughTask) in 28 | Publishers.ConcatenateMany([self.successPublisher] + elements.map({ $0.successPublisher })) 29 | .reduceAndMapTo(()) 30 | .sinkResult { result in 31 | switch result { 32 | case .success(let value): 33 | task.succeed(with: value) 34 | case .failure(let error): 35 | task.fail(with: error) 36 | } 37 | } 38 | } 39 | .eraseToAnyTask() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/SwiftUI/Task+EnvironmentValues.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | import SwiftUI 8 | 9 | extension EnvironmentValues { 10 | struct TaskInterruptibleEnvironmentKey: EnvironmentKey { 11 | static let defaultValue: Bool = true 12 | } 13 | 14 | struct TaskRestartableEnvironmentKey: EnvironmentKey { 15 | static let defaultValue: Bool = true 16 | } 17 | 18 | var taskInterruptible: Bool { 19 | get { 20 | self[TaskInterruptibleEnvironmentKey.self] 21 | } 22 | set { 23 | self[TaskInterruptibleEnvironmentKey.self] = newValue 24 | } 25 | } 26 | 27 | var taskRestartable: Bool { 28 | get { 29 | self[TaskRestartableEnvironmentKey.self] 30 | } 31 | set { 32 | self[TaskRestartableEnvironmentKey.self] = newValue 33 | } 34 | } 35 | } 36 | 37 | // MARK: - API 38 | 39 | extension View { 40 | /// Sets whether tasks controlled by this view are interruptible or not. 41 | /// 42 | /// - Parameters: 43 | /// - interruptible: If `false`, then the view is responsible for disabling user-interaction while its managed task is active. For e.g. if passed as `false` for a `TaskButton`, the button will be disabled while the task is running. 44 | public func taskInterruptible(_ interruptible: Bool) -> some View { 45 | environment(\.taskInterruptible, interruptible) 46 | } 47 | 48 | public func taskRestartable(_ restartable: Bool) -> some View { 49 | environment(\.taskRestartable, restartable) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publisher.prefixUntilAfter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | private enum PrefixUntilAfterOutput { 9 | case output(Output) 10 | case terminate 11 | 12 | public var outputValue: Output? { 13 | if case let .output(output) = self { 14 | return output 15 | } else { 16 | return nil 17 | } 18 | } 19 | 20 | public var isTerminate: Bool { 21 | if case .terminate = self { 22 | return true 23 | } else { 24 | return false 25 | } 26 | } 27 | } 28 | 29 | extension Publisher { 30 | public func prefixUntil( 31 | after predicate: @escaping (Output) -> Bool 32 | ) -> AnyPublisher { 33 | flatMap { output -> AnyPublisher, Failure> in 34 | if predicate(output) { 35 | return Publishers.Concatenate( 36 | prefix: Just(PrefixUntilAfterOutput.output(output)) 37 | .setFailureType(to: Failure.self), 38 | suffix: Just(PrefixUntilAfterOutput.terminate) 39 | .setFailureType(to: Failure.self) 40 | ).eraseToAnyPublisher() 41 | } else { 42 | return Just(PrefixUntilAfterOutput.output(output)) 43 | .setFailureType(to: Failure.self) 44 | .eraseToAnyPublisher() 45 | } 46 | } 47 | .prefix(while: { !$0.isTerminate }) 48 | .compactMap({ $0.outputValue }) 49 | .eraseToAnyPublisher() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/_Concurrency/AsyncStream++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | 7 | extension AsyncStream { 8 | public init( 9 | _ sequence: S 10 | ) where S.Element == Element { 11 | var iterator: S.AsyncIterator? 12 | 13 | self.init { 14 | if iterator == nil { 15 | iterator = sequence.makeAsyncIterator() 16 | } 17 | 18 | do { 19 | return try await iterator?.next() 20 | } catch { 21 | runtimeIssue(error) 22 | 23 | return nil 24 | } 25 | } 26 | } 27 | 28 | public static func streamWithContinuation( 29 | _ elementType: Element.Type = Element.self, 30 | bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded 31 | ) async -> (stream: Self, continuation: Continuation) { 32 | await withAsyncUnsafeContinuation { continuationContinuation in 33 | return Self(elementType, bufferingPolicy: limit) { 34 | continuationContinuation.resume(returning: $0) 35 | } 36 | } 37 | } 38 | 39 | /// An `AsyncStream` that never emits and never completes unless cancelled. 40 | public static var never: Self { 41 | Self { _ in } 42 | } 43 | 44 | /// An `AsyncStream` that never emits and completes immediately. 45 | public static var finished: Self { 46 | Self { $0.finish() } 47 | } 48 | } 49 | 50 | extension AsyncSequence { 51 | public func eraseToStream() -> AsyncStream { 52 | AsyncStream(self) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/AnySubject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | /// A subject that performs type erasure by wrapping another subject. 9 | public final class AnySubject: Subject { 10 | public let base: Any 11 | 12 | @usableFromInline 13 | let _baseAsAnyPublisher: AnyPublisher 14 | @usableFromInline 15 | let _sendImpl: (Output) -> () 16 | @usableFromInline 17 | let _sendCompletionImpl: (Subscribers.Completion) -> () 18 | @usableFromInline 19 | let _sendSubscriptionImpl: (Subscription) -> () 20 | 21 | public init(_ subject: S) where S.Output == Output, S.Failure == Failure { 22 | base = subject 23 | 24 | _baseAsAnyPublisher = subject.eraseToAnyPublisher() 25 | _sendImpl = subject.send 26 | _sendCompletionImpl = subject.send 27 | _sendSubscriptionImpl = subject.send 28 | } 29 | 30 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 31 | _baseAsAnyPublisher.receive(subscriber: subscriber) 32 | } 33 | 34 | public func send(_ value: Output) { 35 | _sendImpl(value) 36 | } 37 | 38 | public func send(completion: Subscribers.Completion) { 39 | _sendCompletionImpl(completion) 40 | } 41 | 42 | public func send(subscription: Subscription) { 43 | _sendSubscriptionImpl(subscription) 44 | } 45 | } 46 | 47 | // MARK: - Auxiliary 48 | 49 | extension Subject { 50 | public func eraseToAnySubject() -> AnySubject { 51 | .init(self) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Darwin/OSUnfairLock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Darwin 6 | import Swallow 7 | 8 | /// An `os_unfair_lock` wrapper. 9 | public final class OSUnfairLock: Initiable, @unchecked Sendable, TestableLock { 10 | @usableFromInline 11 | let base: os_unfair_lock_t 12 | 13 | public init() { 14 | let base = os_unfair_lock_t.allocate(capacity: 1) 15 | 16 | base.initialize(repeating: os_unfair_lock_s(), count: 1) 17 | 18 | self.base = base 19 | } 20 | 21 | @inlinable 22 | public func acquireOrBlock() { 23 | os_unfair_lock_lock(base) 24 | } 25 | 26 | @inlinable 27 | public func acquireOrFail() throws { 28 | let didAcquire = os_unfair_lock_trylock(base) 29 | 30 | if !didAcquire { 31 | throw AcquisitionError.failedToAcquireLock 32 | } 33 | } 34 | 35 | @inlinable 36 | public func relinquish() { 37 | os_unfair_lock_unlock(base) 38 | } 39 | 40 | deinit { 41 | base.deinitialize(count: 1) 42 | base.deallocate() 43 | } 44 | } 45 | 46 | extension OSUnfairLock { 47 | @discardableResult 48 | @inlinable 49 | @inline(__always) 50 | public func withCriticalScope( 51 | perform action: () -> Result 52 | ) -> Result { 53 | defer { 54 | relinquish() 55 | } 56 | 57 | acquireOrBlock() 58 | 59 | return action() 60 | } 61 | } 62 | 63 | // MARK: - Error Handling 64 | 65 | extension OSUnfairLock { 66 | @usableFromInline 67 | enum AcquisitionError: Error { 68 | case failedToAcquireLock 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publishers.While.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Dispatch 7 | import Swallow 8 | 9 | extension Publishers { 10 | public struct While: Publisher { 11 | public typealias Output = DeferredPublisher.Output 12 | public typealias Failure = DeferredPublisher.Failure 13 | 14 | private let queue = DispatchQueue(label: Self.self) 15 | 16 | public let condition: () -> Bool 17 | public let createPublisher: () -> DeferredPublisher 18 | 19 | public init( 20 | condition: @escaping () -> Bool, 21 | createPublisher: @escaping () -> DeferredPublisher 22 | ) { 23 | self.condition = condition 24 | self.createPublisher = createPublisher 25 | } 26 | 27 | public init( 28 | _ condition: @autoclosure @escaping () -> Bool, 29 | createPublisher: @escaping () -> DeferredPublisher 30 | ) { 31 | self.init(condition: condition, createPublisher: createPublisher) 32 | } 33 | 34 | public func receive( 35 | subscriber: S 36 | ) where S.Input == Output, S.Failure == Failure { 37 | if condition() { 38 | createPublisher() 39 | .append(Publishers.While(condition: condition, createPublisher: createPublisher).subscribe(on: queue)) 40 | .receive(subscriber: subscriber) 41 | } else { 42 | Empty(completeImmediately: true) 43 | .receive(subscriber: subscriber) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Concurrency/_UnsafeActorIsolated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | public actor _UnsafeActorIsolated: Sendable { 9 | private let mutex = _AsyncActorSemaphore.Lock() 10 | private var stream = AsyncPassthroughStream() 11 | 12 | public var value: Value 13 | 14 | public func withCriticalRegion(_ body: (inout Value) -> Void) async { 15 | await acquire() 16 | 17 | body(&value) 18 | 19 | stream.send(value) 20 | 21 | await relinquish() 22 | } 23 | 24 | public func update(_ value: Value) async { 25 | await withCriticalRegion { 26 | $0 = value 27 | } 28 | } 29 | 30 | public init(_ value: Value) { 31 | self.value = value 32 | } 33 | 34 | private func acquire() async { 35 | await mutex.acquire() 36 | } 37 | 38 | private func relinquish() async { 39 | await mutex.relinquish() 40 | 41 | stream.finish() 42 | 43 | stream = .init() 44 | } 45 | } 46 | 47 | extension _UnsafeActorIsolated { 48 | public func changesUntilRelinquished() async throws -> AsyncThrowingStream { 49 | try await mutex.acquireOrFail() 50 | 51 | let iteratorBox = ReferenceBox(await stream.makeAsyncIterator()) 52 | let iteratorBoxBox = _UncheckedSendable(iteratorBox) 53 | 54 | let stream = AsyncThrowingStream(unfolding: { 55 | try await iteratorBoxBox.wrappedValue.wrappedValue.next() 56 | }) 57 | 58 | await mutex.relinquish() 59 | 60 | return stream 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/KeyedThrowingTaskGroupTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | @testable import Merge 6 | 7 | import Swallow 8 | import XCTest 9 | 10 | final class KeyedThrowingTaskGroupTests: XCTestCase { 11 | func testUseExistingPolicy() async throws { 12 | let graph = KeyedThrowingTaskGroup() 13 | 14 | try graph.insert(.foo) { 15 | try await Task.sleep(.milliseconds(200)) 16 | 17 | return 1 18 | } 19 | 20 | let existingResult = try await graph.perform(.foo, policy: .useExisting) { 21 | try await Task.sleep(.milliseconds(200)) 22 | 23 | return 2 24 | } 25 | 26 | XCTAssertEqual(existingResult, 1) 27 | 28 | let freshResult = try await graph.perform(.foo, policy: .useExisting) { 29 | try await Task.sleep(.milliseconds(200)) 30 | 31 | return 3 32 | } 33 | 34 | XCTAssertEqual(freshResult, 3) 35 | } 36 | 37 | func testUnspecifiedInsertionPolicyFailure() async throws { 38 | let graph = KeyedThrowingTaskGroup() 39 | 40 | func insertLongFoo() async throws { 41 | try graph.insert(.foo) { 42 | try await Task.sleep(.seconds(10)) 43 | } 44 | } 45 | 46 | try await insertLongFoo() 47 | 48 | var caughtError: Error? 49 | 50 | do { 51 | try await insertLongFoo() 52 | } catch { 53 | caughtError = error 54 | } 55 | 56 | XCTAssertNotNil(caughtError) 57 | } 58 | } 59 | 60 | fileprivate enum TestTasks: Hashable, Sendable { 61 | case foo 62 | case bar 63 | case baz 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular (WIP)/Actor Effects/_ActorSideEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Diagnostics 6 | import Swallow 7 | 8 | public protocol _ActorSideEffectSpecification { 9 | 10 | } 11 | 12 | public protocol _ActorSideEffect { 13 | 14 | } 15 | 16 | public protocol _ActorSideEffectSpecifying { 17 | associatedtype EffectSpecificationType: _ActorSideEffectSpecification 18 | 19 | static var effectSpecification: EffectSpecificationType { get } 20 | } 21 | 22 | public struct _ResolvedTaskEffect { 23 | 24 | } 25 | 26 | public protocol _ActorSideEffectModifier { 27 | @_spi(Internal) 28 | func _modify(_: inout _ResolvedTaskEffect) 29 | } 30 | 31 | struct _ModifiedActorTaskEffectSpecification: _ActorSideEffectSpecification { 32 | let content: Content 33 | let modifier: Modifier 34 | } 35 | 36 | extension _ActorSideEffectSpecification { 37 | public func _modifier(_ modifier: some _ActorSideEffectModifier) -> some _ActorSideEffectSpecification { 38 | _ModifiedActorTaskEffectSpecification(content: self, modifier: modifier) 39 | } 40 | } 41 | 42 | public struct _ActorSideEffectsSpecifications { 43 | public enum _ActorSideEffectSpecificationSymbol { 44 | case keyPath(AnyKeyPath) 45 | } 46 | 47 | /// Apply an effect on the change of something. 48 | public struct OnChange: _ActorSideEffectSpecification { 49 | public let value: _ActorSideEffectSpecificationSymbol 50 | public let content: Content 51 | 52 | init(value: _ActorSideEffectSpecificationSymbol, content: Content) { 53 | self.value = value 54 | self.content = content 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/_AsyncObjectWillChangePublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | public final class _AsyncObjectWillChangePublisher: Publisher, Sendable { 9 | public typealias Output = ObservableObjectPublisher.Output 10 | public typealias Failure = ObservableObjectPublisher.Failure 11 | 12 | private let base: Publishers.CountSubscribers 13 | 14 | public init() { 15 | self.base = .init(upstream: .init()) 16 | } 17 | 18 | public func receive>( 19 | subscriber: S 20 | ) { 21 | base.receive(subscriber: subscriber) 22 | } 23 | } 24 | 25 | extension _AsyncObjectWillChangePublisher { 26 | public func withCriticalScope( 27 | _ f: @escaping (ObservableObjectPublisher) -> Void 28 | ) { 29 | let shouldExit = base.withGuaranteedSubscriberCount { count -> Bool in 30 | guard count == 0 else { 31 | return false 32 | } 33 | 34 | f(self.base.upstream) 35 | 36 | return true 37 | } 38 | 39 | if shouldExit { 40 | return 41 | } 42 | 43 | MainThreadScheduler.shared.schedule { 44 | f(self.base.upstream) 45 | } 46 | } 47 | 48 | public func run( 49 | _ f: @escaping @MainActor () -> Void 50 | ) async { 51 | let runOnMainThread = base.withGuaranteedSubscriberCount { count -> Bool in 52 | return count != 0 53 | } 54 | 55 | if runOnMainThread { 56 | await MainActor.run { 57 | f() 58 | } 59 | } else { 60 | await f() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/Foundation/ObservableTasks.Map.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Foundation 7 | import Swift 8 | 9 | extension ObservableTasks { 10 | public final class Map: ObservableTask { 11 | public typealias Success = Success 12 | public typealias Error = Upstream.Error 13 | public typealias Status = ObservableTaskStatus 14 | 15 | private let upstream: Upstream 16 | private let transform: (Upstream.Success) -> Success 17 | 18 | public var objectWillChange: AnyObjectWillChangePublisher { 19 | .init(from: upstream) 20 | } 21 | 22 | public var objectDidChange: AnyPublisher { 23 | let transform = self.transform 24 | 25 | return upstream.objectDidChange.map({ $0.map(transform) }).eraseToAnyPublisher() 26 | } 27 | 28 | public init( 29 | upstream: Upstream, 30 | transform: @escaping (Upstream.Success) -> Success 31 | ) { 32 | self.upstream = upstream 33 | self.transform = transform 34 | } 35 | 36 | public var status: ObservableTaskStatus { 37 | upstream.status.map(transform) 38 | } 39 | 40 | public func start() { 41 | upstream.start() 42 | } 43 | 44 | public func cancel() { 45 | upstream.cancel() 46 | } 47 | 48 | } 49 | } 50 | 51 | // MARK: - API 52 | 53 | extension ObservableTask { 54 | public func map( 55 | _ transform: @escaping (Success) -> T 56 | ) -> ObservableTasks.Map { 57 | ObservableTasks.Map(upstream: self, transform: transform) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/AnyFuture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | /// A single-output publisher that performs type erasure by wrapping another single-output publisher. 9 | public struct AnySingleOutputPublisher: SingleOutputPublisher { 10 | public let base: AnyPublisher 11 | 12 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 13 | base.receive(subscriber: subscriber) 14 | } 15 | } 16 | 17 | // MARK: - API 18 | 19 | extension AnySingleOutputPublisher { 20 | public init(_unsafe publisher: P) where P.Output == Output, P.Failure == Failure { 21 | self.base = publisher.eraseToAnyPublisher() 22 | } 23 | 24 | public init(_ publisher: P) where P.Output == Output, P.Failure == Failure { 25 | self.base = publisher.eraseToAnyPublisher() 26 | } 27 | } 28 | 29 | extension AnySingleOutputPublisher { 30 | public static func result(_ result: Result) -> Self { 31 | AnyPublisher.result(result)._unsafe_eraseToAnySingleOutputPublisher() 32 | } 33 | 34 | public static func just(_ output: Output) -> Self { 35 | AnyPublisher.just(output)._unsafe_eraseToAnySingleOutputPublisher() 36 | } 37 | 38 | public static func failure(_ failure: Failure) -> Self { 39 | AnyPublisher.failure(failure)._unsafe_eraseToAnySingleOutputPublisher() 40 | } 41 | } 42 | 43 | extension Publisher { 44 | public func _unsafe_eraseToAnySingleOutputPublisher() -> AnySingleOutputPublisher { 45 | .init(_unsafe: self) 46 | } 47 | } 48 | 49 | extension SingleOutputPublisher { 50 | public func eraseToAnySingleOutputPublisher() -> AnySingleOutputPublisher { 51 | .init(self) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/Foundation/TaskSuccessPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | /// A publisher that delivers the result of a task. 8 | public struct TaskSuccessPublisher { 9 | private let upstream: Upstream 10 | 11 | public init(upstream: Upstream) { 12 | self.upstream = upstream 13 | } 14 | 15 | public func receive( 16 | subscriber: S 17 | ) where S.Input == Output, S.Failure == Failure { 18 | upstream 19 | .outputPublisher 20 | .compactMap({ $0.value }) 21 | .receive(subscriber: subscriber) 22 | } 23 | } 24 | 25 | extension TaskSuccessPublisher: SingleOutputPublisher { 26 | public typealias Output = Upstream.Success 27 | public typealias Failure = ObservableTaskFailure 28 | } 29 | 30 | // MARK: - API 31 | 32 | extension ObservableTask { 33 | /// A publisher that delivers the result of a task. 34 | public var successPublisher: TaskSuccessPublisher { 35 | .init(upstream: self) 36 | } 37 | 38 | /// The successful result of a task, after it completes. 39 | /// 40 | /// - returns: The task's successful result. 41 | /// - throws: An error indicating task failure or task cancellation. 42 | public var value: Success { 43 | get async throws { 44 | do { 45 | let result: Success = try await successPublisher.output() 46 | 47 | return result 48 | } catch { 49 | if let error = error as? ObservableTaskFailureProtocol { 50 | if let unwrappedError: any Swift.Error = error._opaque_error { 51 | throw unwrappedError 52 | } 53 | } 54 | 55 | throw error 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Mutex/Lock/Lock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | 7 | public protocol Lock: ScopedMutexProtocol, Sendable { 8 | func acquireOrBlock() 9 | func relinquish() 10 | } 11 | 12 | public protocol TestableLock: Lock, TestableScopedMutexProtocol { 13 | var hasBeenAcquired: Bool { get } 14 | 15 | func acquireOrFail() throws 16 | } 17 | 18 | public protocol ReentrantLock: Lock, ReentrantMutexProtocol { 19 | 20 | } 21 | 22 | // MARK: - Implementation 23 | 24 | extension Lock { 25 | @_transparent 26 | @discardableResult 27 | @inlinable 28 | public func withCriticalScope( 29 | _ f: (() throws -> T) 30 | ) rethrows -> T { 31 | defer { 32 | relinquish() 33 | } 34 | 35 | acquireOrBlock() 36 | 37 | return try f() 38 | } 39 | } 40 | 41 | extension TestableLock { 42 | public var hasBeenAcquired: Bool { 43 | if let _ = try? acquireOrFail() { 44 | relinquish() 45 | return false 46 | } else { 47 | return true 48 | } 49 | } 50 | 51 | @discardableResult 52 | public func attemptWithCriticalScope(_ f: (() throws -> T)) rethrows -> T? { 53 | do { 54 | try acquireOrFail() 55 | 56 | let result = Result(catching: { try f() }) 57 | 58 | relinquish() 59 | 60 | return try result.get() 61 | } catch { 62 | return nil 63 | } 64 | } 65 | } 66 | 67 | // MARK: - Conformances 68 | 69 | public final class AnyLock: Lock { 70 | public let base: Lock 71 | 72 | public init(_ base: L) { 73 | self.base = base 74 | } 75 | 76 | public func acquireOrBlock() { 77 | base.acquireOrBlock() 78 | } 79 | 80 | public func relinquish() { 81 | base.relinquish() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/AnyObservableObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | public final class AnyObservableObject: ObservableObject { 9 | public let base: any ObservableObject 10 | public let objectWillChange: AnyPublisher 11 | 12 | public init( 13 | _ base: T 14 | ) where T.ObjectWillChangePublisher.Output == Output, T.ObjectWillChangePublisher.Failure == Failure { 15 | self.base = base 16 | self.objectWillChange = base.objectWillChange.eraseToAnyPublisher() 17 | } 18 | 19 | public init( 20 | _ base: any _opaque_ObservableObject 21 | ) where Output == AnyObjectWillChangePublisher.Output, Failure == AnyObjectWillChangePublisher.Failure { 22 | self.base = base as (any ObservableObject) 23 | self.objectWillChange = base._opaque_objectWillChange.eraseToAnyPublisher() 24 | } 25 | 26 | fileprivate init( 27 | _erasing base: T 28 | ) where Output == AnyObjectWillChangePublisher.Output, Failure == AnyObjectWillChangePublisher.Failure { 29 | self.base = base as (any ObservableObject) 30 | self.objectWillChange = base._opaque_objectWillChange.eraseToAnyPublisher() 31 | } 32 | } 33 | 34 | extension AnyObservableObject where Output == Void, Failure == Never { 35 | public static var empty: AnyObservableObject { 36 | .init(_EmptyObservableObject()) 37 | } 38 | } 39 | 40 | // MARK: - Auxiliary 41 | 42 | private final class _EmptyObservableObject: ObservableObject { 43 | 44 | } 45 | 46 | extension ObservableObject { 47 | public func _eraseToAnyObservableObject() -> _AnyObservableObject { 48 | _AnyObservableObject(_erasing: self) 49 | } 50 | } 51 | 52 | public typealias _AnyObservableObject = AnyObservableObject 53 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "ad35f7cce3eb38ad5e7d55cff5e9b28638dc499db2f0744a61d9b321948bbefd", 3 | "pins" : [ 4 | { 5 | "identity" : "swallow", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/vmanot/Swallow.git", 8 | "state" : { 9 | "branch" : "master", 10 | "revision" : "6a996c765a000e6c744276300ac5c01c86070fe1" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-atomics", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/apple/swift-atomics.git", 17 | "state" : { 18 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 19 | "version" : "1.2.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-collections", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-collections", 26 | "state" : { 27 | "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", 28 | "version" : "1.2.0" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-subprocess", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/preternatural-fork/swift-subprocess.git", 35 | "state" : { 36 | "branch" : "main", 37 | "revision" : "80bd50f91abf49164156b61c1071821927242c62" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-syntax", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/swift-precompiled/swift-syntax", 44 | "state" : { 45 | "branch" : "release/6.1", 46 | "revision" : "fc197a24fb2e77609fbe3d94624e36f84d758099" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-system", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/apple/swift-system", 53 | "state" : { 54 | "revision" : "61e4ca4b81b9e09e2ec863b00c340eb13497dac6", 55 | "version" : "1.5.0" 56 | } 57 | } 58 | ], 59 | "version" : 3 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/RetainUntilCancel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Dispatch 7 | import Swift 8 | 9 | public final class RetainUntilCancel: Cancellable { 10 | @usableFromInline 11 | var instance: RetainUntilCancel? 12 | 13 | @usableFromInline 14 | var child: Child? 15 | 16 | public init(_ cancellable: Child) { 17 | instance = self 18 | child = cancellable 19 | } 20 | 21 | @inlinable 22 | public func cancel() { 23 | instance = nil 24 | 25 | child?.cancel() 26 | child = nil 27 | } 28 | } 29 | 30 | // MARK: - API 31 | 32 | extension Publisher { 33 | @discardableResult 34 | @inlinable 35 | public func retainSink( 36 | receiveCompletion: @escaping ((Subscribers.Completion) -> Void), 37 | receiveValue: @escaping ((Output) -> Void) 38 | ) -> RetainUntilCancel { 39 | let _cancellable = SingleAssignmentAnyCancellable() 40 | let cancellable = RetainUntilCancel(_cancellable) 41 | 42 | _cancellable.set( 43 | handleCancelOrCompletion { _ in 44 | cancellable.cancel() 45 | } 46 | .handleOutput(receiveValue) 47 | .sink() 48 | ) 49 | 50 | return cancellable 51 | } 52 | 53 | @discardableResult 54 | @inlinable 55 | public func retainSink() -> RetainUntilCancel { 56 | retainSink( 57 | receiveCompletion: { _ in }, 58 | receiveValue: { _ in } 59 | ) 60 | } 61 | 62 | @discardableResult 63 | @inlinable 64 | public func retainSink( 65 | receiveValue: @escaping ((Output) -> Void) 66 | ) -> RetainUntilCancel where Failure == Never { 67 | retainSink( 68 | receiveCompletion: { _ in }, 69 | receiveValue: receiveValue 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/_Concurrency/AsyncSequence++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | import SwallowMacrosClient 7 | 8 | extension AsyncSequence { 9 | public func collect() async rethrows -> Array { 10 | try await reduce(into: Array()) { 11 | $0.append($1) 12 | } 13 | } 14 | } 15 | 16 | extension AsyncSequence { 17 | public func first() async rethrows -> Element? { 18 | try await first { _ in true } 19 | } 20 | 21 | @_disfavoredOverload 22 | public func first( 23 | byUnwrapping transform: (Element) throws -> T? 24 | ) async rethrows -> T? { 25 | for try await element in self { 26 | if let match = try transform(element) { 27 | return match 28 | } 29 | } 30 | 31 | return nil 32 | } 33 | 34 | @_disfavoredOverload 35 | public func firstAndOnly( 36 | byUnwrapping transform: (Element) throws -> T? 37 | ) async throws -> T? { 38 | var result: T? 39 | 40 | for try await element in self { 41 | if let match = try transform(element) { 42 | guard result == nil else { 43 | #throw 44 | } 45 | 46 | result = match 47 | } 48 | } 49 | 50 | return result 51 | } 52 | 53 | public func first(ofType type: T.Type) async throws -> T? { 54 | try await first(byUnwrapping: { $0 as? T }) 55 | } 56 | 57 | public func firstAndOnly(ofType type: T.Type) async throws -> T? { 58 | try await firstAndOnly(byUnwrapping: { $0 as? T }) 59 | } 60 | 61 | public func firstAndOnly( 62 | where predicate: (Element) throws -> Bool 63 | ) async throws -> Element? { 64 | try await firstAndOnly(byUnwrapping: { try predicate($0) ? $0 : nil }) 65 | } 66 | } 67 | 68 | extension AsyncSequence { 69 | public func eraseToThrowingStream() -> AsyncThrowingStream { 70 | AsyncThrowingStream(self) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Shell Scripting/SystemShell.run.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swallow 7 | 8 | @available(macOS 11.0, *) 9 | @available(iOS, unavailable) 10 | @available(macCatalyst, unavailable) 11 | @available(tvOS, unavailable) 12 | @available(watchOS, unavailable) 13 | extension SystemShell { 14 | public func run( 15 | shell: SystemShell.Environment, 16 | command: String 17 | ) async throws -> _ProcessRunResult { 18 | try await run( 19 | executableURL: shell.launchURL.unwrap(), 20 | arguments: shell.deriveArguments(command), 21 | environment: shell 22 | ) 23 | } 24 | 25 | public func run( 26 | executablePath: String, 27 | arguments: [String], 28 | environment: Environment = .zsh 29 | ) async throws -> _ProcessRunResult { 30 | try await run( 31 | executableURL: try URL(string: executablePath).unwrap(), 32 | arguments: arguments, 33 | environment: environment 34 | ) 35 | } 36 | } 37 | 38 | @available(macOS 11.0, *) 39 | @available(iOS, unavailable) 40 | @available(macCatalyst, unavailable) 41 | @available(tvOS, unavailable) 42 | @available(watchOS, unavailable) 43 | extension SystemShell { 44 | @discardableResult 45 | public static func run( 46 | command: String, 47 | input: String? = nil, 48 | environment: Environment = .zsh, 49 | environmentVariables: [String: String] = [:], 50 | currentDirectoryURL: URL? = nil, 51 | options: [_AsyncProcess.Option]? = nil 52 | ) async throws -> _ProcessRunResult { 53 | let shell = SystemShell(options: options) 54 | 55 | shell.environmentVariables.merge(environmentVariables, uniquingKeysWith: { lhs, rhs in rhs }) 56 | shell.currentDirectoryURL = currentDirectoryURL 57 | 58 | let result: _ProcessRunResult = try await shell.run( 59 | command: command, 60 | input: input, 61 | environment: environment 62 | ) 63 | 64 | return result 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/Publishers.FlatMap+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | extension Publisher { 9 | @_disfavoredOverload 10 | public func flatMap

( 11 | maxPublishers: Subscribers.Demand = .unlimited, 12 | _ transform: @escaping (Self.Output) -> P 13 | ) -> Publishers.FlatMap> { 14 | .init(upstream: self.setFailureType(to: P.Failure.self), maxPublishers: maxPublishers, transform: transform) 15 | } 16 | 17 | @_disfavoredOverload 18 | public func flatMap( 19 | maxPublishers: Subscribers.Demand = .unlimited, 20 | _ transform: @escaping (Output) -> P 21 | ) -> Publishers.FlatMap, Publishers.MapError> { 22 | eraseError().flatMap(maxPublishers: maxPublishers) { output in 23 | transform(output).eraseError() 24 | } 25 | } 26 | } 27 | 28 | extension Publisher where Failure == Error { 29 | public func flatMap( 30 | maxPublishers: Subscribers.Demand = .unlimited, 31 | _ transform: @escaping (Output) -> P 32 | ) -> Publishers.FlatMap, Self> { 33 | flatMap(maxPublishers: maxPublishers) { output in 34 | transform(output) 35 | .eraseError() 36 | } 37 | } 38 | 39 | public func flatMap( 40 | _ keyPath: KeyPath, 41 | maxPublishers: Subscribers.Demand = .unlimited, 42 | _ transform: @escaping (T) -> P 43 | ) -> Publishers.FlatMap, Self> { 44 | flatMap(maxPublishers: maxPublishers) { output in 45 | transform(output[keyPath: keyPath]) 46 | .eraseError() 47 | } 48 | } 49 | 50 | public func flatMap( 51 | from publisher: P 52 | ) -> Publishers.FlatMap, Publishers.MapError> { 53 | publisher 54 | .eraseError() 55 | .flatMap { _ in self } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SwiftDI/Intramodular/Core/TaskDependency-Initializers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | 7 | extension TaskDependency { 8 | public init() { 9 | self.init( 10 | initialTaskDependencies: TaskDependencies.current, 11 | resolveValue: { try $0.resolve(.unkeyed(Value.self)) } 12 | ) 13 | } 14 | 15 | public init() where Value == Optional { 16 | self.init( 17 | initialTaskDependencies: TaskDependencies.current, 18 | resolveValue: { try $0.resolve(.unkeyed(T.self)) } 19 | ) 20 | } 21 | 22 | public init( 23 | _ keyPath: KeyPath 24 | ) { 25 | self.init( 26 | initialTaskDependencies: TaskDependencies.current, 27 | resolveValue: { $0[keyPath] } 28 | ) 29 | } 30 | 31 | @_disfavoredOverload 32 | public init( 33 | _ keyPath: KeyPath> 34 | ) { 35 | self.init( 36 | initialTaskDependencies: TaskDependencies.current, 37 | resolveValue: { $0[keyPath] } 38 | ) 39 | } 40 | 41 | public init( 42 | _ keyPath: KeyPath> 43 | ) where Value == Optional { 44 | self.init( 45 | initialTaskDependencies: TaskDependencies.current, 46 | resolveValue: { $0[keyPath] } 47 | ) 48 | } 49 | 50 | public init( 51 | _ keyPath: KeyPath, 52 | _resolve resolve: @escaping (T) throws -> Optional 53 | ) { 54 | self.init( 55 | initialTaskDependencies: TaskDependencies.current, 56 | resolveValue: { try resolve($0[keyPath]) } 57 | ) 58 | } 59 | 60 | public init( 61 | _ keyPath: KeyPath, 62 | as type: Value.Type 63 | ) { 64 | self.init( 65 | initialTaskDependencies: TaskDependencies.current, 66 | resolveValue: { try cast($0[keyPath], to: Value.self) } 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/Foundation/ObservableTasks.HandleEvents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swift 7 | 8 | extension ObservableTasks { 9 | public final class HandleEvents: ObservableTask { 10 | public typealias Success = Base.Success 11 | public typealias Error = Base.Error 12 | 13 | private let base: Base 14 | private let receiveStart: (() -> Void)? 15 | private let receiveCancel: (() -> Void)? 16 | 17 | public var status: ObservableTaskStatus { 18 | base.status 19 | } 20 | 21 | public var objectWillChange: Base.ObjectWillChangePublisher { 22 | base.objectWillChange 23 | } 24 | 25 | public var objectDidChange: Base.ObjectDidChangePublisher { 26 | base.objectDidChange 27 | } 28 | 29 | public init( 30 | base: Base, 31 | receiveStart: (() -> Void)? = nil, 32 | receiveCancel: (() -> Void)? = nil 33 | ) { 34 | self.base = base 35 | 36 | self.receiveStart = receiveStart 37 | self.receiveCancel = receiveCancel 38 | } 39 | 40 | public func start() { 41 | if status == .idle { 42 | receiveStart?() 43 | } 44 | 45 | base.start() 46 | } 47 | 48 | public func cancel() { 49 | if status != .canceled || status != .success { 50 | receiveCancel?() 51 | } 52 | 53 | base.cancel() 54 | } 55 | } 56 | } 57 | 58 | // MARK: - API 59 | 60 | extension ObservableTask { 61 | public func handleEvents( 62 | receiveStart: (() -> Void)? = nil, 63 | receiveCancel: (() -> Void)? = nil 64 | ) -> ObservableTasks.HandleEvents { 65 | ObservableTasks.HandleEvents( 66 | base: self, 67 | receiveStart: receiveStart, 68 | receiveCancel: receiveCancel 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/_opaque_VoidSender.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Runtime 7 | 8 | public protocol _opaque_VoidSender: AnyObject { 9 | func send() 10 | } 11 | 12 | // MARK: - Conformances 13 | 14 | extension CurrentValueSubject: _opaque_VoidSender where Output == Void { 15 | 16 | } 17 | 18 | extension ObservableObjectPublisher: _opaque_VoidSender { 19 | 20 | } 21 | 22 | extension PassthroughSubject: _opaque_VoidSender where Output == Void { 23 | 24 | } 25 | 26 | // MARK: - Helpers 27 | 28 | extension Publisher where Failure == Never { 29 | @inlinable 30 | public func publish( 31 | to object: any _opaque_ObservableObject 32 | ) -> Publishers.HandleEvents { 33 | return handleEvents(receiveOutput: { [weak object] _ in 34 | try! object?._opaque_objectWillChange_send() 35 | }) 36 | } 37 | 38 | @inlinable 39 | public func publish( 40 | to object: T 41 | ) -> Publishers.HandleEvents where T.ObjectWillChangePublisher == Combine.ObservableObjectPublisher { 42 | handleEvents(receiveOutput: { [weak object] _ in 43 | object?.objectWillChange.send() 44 | }) 45 | } 46 | } 47 | 48 | extension Publisher where Output == Void, Failure == Never { 49 | @inlinable 50 | public func publish( 51 | to publisher: _opaque_VoidSender 52 | ) -> Publishers.HandleEvents { 53 | handleEvents(receiveOutput: { [weak publisher] _ in 54 | guard let publisher = publisher else { 55 | assertionFailure() 56 | 57 | return 58 | } 59 | 60 | publisher.send() 61 | }) 62 | } 63 | } 64 | 65 | @_spi(Internal) 66 | public func _ObservableObject_objectWillChange_send(_ x: T) { 67 | guard let x = x as? (any ObservableObject) else { 68 | return 69 | } 70 | 71 | guard let x = (x.objectWillChange as (any Publisher)) as? _opaque_VoidSender else { 72 | assertionFailure() 73 | 74 | return 75 | } 76 | 77 | x.send() 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Foundation/ProcessPublisher-EnvSupport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | #if os(macOS) 6 | 7 | import Foundation 8 | 9 | /// The URL of `env` on a UNIXy system, used to execute commands via `$PATH`. 10 | internal let envExecutableURL: URL = URL(fileURLWithPath: "/usr/bin/env") 11 | 12 | /// Checks that the given arguments are appropriate for passing to `env`. 13 | /// 14 | /// - There must be a command to run. 15 | /// - No options can be passed to `env`. 16 | /// - No new environment variables can be passed to `env`. 17 | /// 18 | /// (The library *could* support some of these, but that would be an abstraction 19 | /// violation, preventing future changes and leading clients to use the 20 | /// `PATH`-based APIs when they really shouldn't.) 21 | internal func validateArgumentsForEnv(_ arguments: [String]) { 22 | guard let first = arguments.first else { 23 | preconditionFailure("must provide the name of a command to run") 24 | } 25 | 26 | precondition(!first.starts(with: "-"), "command to run must not start with '-'") 27 | precondition(!first.contains("="), "command to run cannot have '=' in its name") 28 | } 29 | 30 | /// Checks that `env` didn't fail because it couldn't find the command. 31 | /// 32 | /// If that *is* why it failed, it's considered a programmer error and the 33 | /// program will be aborted. 34 | internal func makeErrorCheckerForEnv( 35 | _ arguments: [String], 36 | conversion: @escaping (ProcessExitFailure) -> Failure 37 | ) -> (ProcessExitFailure) -> Failure { 38 | let command = arguments.first! 39 | return { error in 40 | let commandNotFoundByEnvStatus: CInt = 127 41 | if case .exit(status: commandNotFoundByEnvStatus) = error { 42 | do { 43 | let check = try Process.run(URL(fileURLWithPath: "/usr/bin/which"), arguments: ["-s", command]) { 44 | precondition($0.terminationStatus == EXIT_SUCCESS, "command '\(command)' not found") 45 | } 46 | check.waitUntilExit() 47 | } catch { 48 | // Okay, if we failed to call `which` for some reason, just give up. 49 | } 50 | } 51 | return conversion(error) 52 | } 53 | } 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Process/Process.PipeName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import FoundationX 6 | import Swift 7 | import System 8 | 9 | public enum _ProcessPipeName: Codable, Hashable, Sendable { 10 | case standardInput 11 | case standardOutput 12 | case standardError 13 | } 14 | 15 | #if os(macOS) 16 | 17 | extension Process { 18 | public typealias PipeName = _ProcessPipeName 19 | } 20 | 21 | // MARK: - Supplementary 22 | 23 | public protocol _ProcessPipeProviding { 24 | func pipe( 25 | named name: Process.PipeName 26 | ) -> Pipe? 27 | } 28 | 29 | extension _ProcessPipeProviding { 30 | public func name(of pipe: Pipe) throws -> Process.PipeName { 31 | if pipe === self.pipe(named: .standardInput) { 32 | return .standardInput 33 | } else if pipe === self.pipe(named: .standardOutput) { 34 | return .standardOutput 35 | } else if pipe === self.pipe(named: .standardError) { 36 | return .standardError 37 | } else { 38 | throw Process.UnrecognizedPipeError.pipe(pipe) 39 | } 40 | } 41 | } 42 | 43 | extension Process: _ProcessPipeProviding { 44 | public func pipe( 45 | named name: PipeName 46 | ) -> Pipe? { 47 | switch name { 48 | case .standardInput: 49 | return standardInput as? Pipe 50 | case .standardOutput: 51 | return standardOutput as? Pipe 52 | case .standardError: 53 | return standardError as? Pipe 54 | } 55 | } 56 | } 57 | 58 | extension _AsyncProcess: _ProcessPipeProviding { 59 | public typealias PipeName = Process.PipeName 60 | 61 | public func pipe( 62 | named name: Process.PipeName 63 | ) -> Pipe? { 64 | switch name { 65 | case .standardInput: 66 | return _standardInputPipe 67 | case .standardOutput: 68 | return _standardOutputPipe 69 | case .standardError: 70 | return _standardErrorPipe 71 | } 72 | } 73 | } 74 | 75 | // MARK: - Error Handling 76 | 77 | extension Process { 78 | public enum UnrecognizedPipeError: Swift.Error, @unchecked Sendable { 79 | case pipe(Pipe) 80 | } 81 | } 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Rate-limiting & Retrying/TaskRetryDelayStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Darwin 6 | import Swallow 7 | 8 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 9 | public protocol TaskRetryDelayStrategy: Hashable, Sendable { 10 | func delay( 11 | forAttempt attempt: Int, 12 | withInitialDelay initial: Duration 13 | ) -> Duration 14 | } 15 | 16 | // MARK: - Standard Implementations 17 | 18 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 19 | public struct LinearBackoffStrategy: TaskRetryDelayStrategy { 20 | public func delay( 21 | forAttempt attempt: Int, 22 | withInitialDelay initial: Duration 23 | ) -> Duration { 24 | initial * Double(attempt) 25 | } 26 | } 27 | 28 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 29 | extension TaskRetryDelayStrategy where Self == LinearBackoffStrategy { 30 | public static var linear: Self { 31 | .init() 32 | } 33 | } 34 | 35 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 36 | public struct ExponentialBackoffStrategy: TaskRetryDelayStrategy { 37 | public let maximumInterval: Duration? 38 | public let jitter: Bool 39 | 40 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 41 | public func delay( 42 | forAttempt attempt: Int, 43 | withInitialDelay initial: Duration 44 | ) -> Duration { 45 | var delay = initial * pow(2, Double(attempt - 1)) 46 | 47 | if let maximumInterval { 48 | delay = min(delay, maximumInterval) 49 | } 50 | 51 | if jitter { 52 | return Duration(_timeInterval: Double.random(in: 0...delay._timeInterval)) 53 | } else { 54 | return delay 55 | } 56 | } 57 | } 58 | 59 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 60 | extension TaskRetryDelayStrategy where Self == ExponentialBackoffStrategy { 61 | public static func exponentialBackoff( 62 | maximumInterval: Duration? = nil, 63 | jitter: Bool = true 64 | ) -> Self { 65 | .init(maximumInterval: nil, jitter: jitter) 66 | } 67 | 68 | public static var exponentialBackoff: Self { 69 | self.exponentialBackoff() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Concurrency/_KeyedUnsafeThrowingContinuations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swift 6 | 7 | public actor _KeyedUnsafeThrowingContinuations { 8 | private var waiters: [Key: [UnsafeContinuation]] = [:] 9 | 10 | public var keys: Set { 11 | Set(waiters.keys) 12 | } 13 | 14 | public init() { 15 | 16 | } 17 | 18 | public func wait( 19 | forKey key: Key 20 | ) async throws -> Value { 21 | return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in 22 | self.waiters[key, default: []].insert(continuation) 23 | } 24 | } 25 | 26 | public func resume( 27 | forKey key: Key, 28 | with value: Value 29 | ) { 30 | guard let continuations = waiters[key], !continuations.isEmpty else { 31 | return 32 | } 33 | 34 | for continuation in continuations { 35 | continuation.resume(returning: value) 36 | } 37 | 38 | waiters[key] = nil 39 | } 40 | 41 | public func cancel(forKey key: Key) { 42 | guard let continuations = waiters[key], !continuations.isEmpty else { 43 | return 44 | } 45 | 46 | for continuation in continuations { 47 | continuation.resume(throwing: CancellationError()) 48 | } 49 | 50 | waiters[key] = nil 51 | } 52 | 53 | public func contains(key: Key) -> Bool { 54 | return waiters[key] != nil 55 | } 56 | 57 | public func cancelAll() { 58 | waiters.forEach { 59 | $0.value.forEach { continuation in 60 | continuation.resume(throwing: CancellationError()) 61 | } 62 | } 63 | waiters.removeAll() 64 | } 65 | 66 | public func resumeAll(returning value: Value) { 67 | waiters.forEach { 68 | $0.value.forEach { continuation in 69 | continuation.resume(returning: value) 70 | } 71 | } 72 | waiters.removeAll() 73 | } 74 | 75 | public func resumeAll() where Value == Void { 76 | resumeAll(returning: ()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Collections/_NaiveDoublyLinkedList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | /// A doubly linked list. 9 | public final class _NaiveDoublyLinkedList { 10 | // first <-> node <-> ... <-> last 11 | private(set) var first: Node? 12 | private(set) var last: Node? 13 | 14 | public var isEmpty: Bool { 15 | last == nil 16 | } 17 | 18 | /// Adds an element to the end of the list. 19 | @discardableResult 20 | public func append(_ element: Element) -> Node { 21 | let node = Node(value: element) 22 | 23 | append(node) 24 | 25 | return node 26 | } 27 | 28 | /// Adds a node to the end of the list. 29 | public func append(_ node: Node) { 30 | if let last = last { 31 | last.next = node 32 | node.previous = last 33 | self.last = node 34 | } else { 35 | last = node 36 | first = node 37 | } 38 | } 39 | 40 | public func remove(_ node: Node) { 41 | node.next?.previous = node.previous // node.previous is nil if node=first 42 | node.previous?.next = node.next // node.next is nil if node=last 43 | if node === last { 44 | last = node.previous 45 | } 46 | if node === first { 47 | first = node.next 48 | } 49 | node.next = nil 50 | node.previous = nil 51 | } 52 | 53 | public func removeAllElements() { 54 | // avoid recursive Nodes deallocation 55 | var node = first 56 | while let next = node?.next { 57 | node?.next = nil 58 | next.previous = nil 59 | node = next 60 | } 61 | 62 | last = nil 63 | first = nil 64 | } 65 | 66 | public final class Node { 67 | public let value: Element 68 | 69 | public fileprivate(set) var next: Node? 70 | public fileprivate(set) var previous: Node? 71 | 72 | fileprivate init(value: Element) { 73 | self.value = value 74 | } 75 | } 76 | 77 | deinit { 78 | // This way we make sure that the deallocations do no happen recursively 79 | // (and potentially overflow the stack). 80 | removeAllElements() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/WIP/_TaskSinkProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | @_spi(Internal) 9 | public protocol _TaskSinkProtocol<_ObservableTaskFailureType> { 10 | associatedtype _ObservableTaskFailureType: Error 11 | associatedtype _ResultFailureType: Error 12 | 13 | func receive( 14 | _ task: Task 15 | ) async -> Result 16 | } 17 | 18 | @_spi(Internal) 19 | extension _TaskSinkProtocol { 20 | @discardableResult 21 | func _opaque_receive( 22 | _ task: Task 23 | ) async -> Result where _ObservableTaskFailureType == Never { 24 | await self.receive(task).mapError({ $0 as Error }) 25 | } 26 | 27 | @discardableResult 28 | func _opaque_receive( 29 | _ task: Task 30 | ) async -> Result where _ObservableTaskFailureType == Swift.Error { 31 | await self.receive(task).mapError({ $0 as Error }) 32 | } 33 | } 34 | 35 | // MARK: - Conformees 36 | 37 | @_spi(Internal) 38 | extension TaskQueue: _TaskSinkProtocol { 39 | public typealias _ObservableTaskFailureType = Never 40 | public typealias _ResultFailureType = CancellationError 41 | 42 | public func receive( 43 | _ task: Task 44 | ) async -> Result { 45 | await _performCancellable { 46 | await task.value 47 | } 48 | } 49 | } 50 | 51 | @_spi(Internal) 52 | extension ThrowingTaskQueue: _TaskSinkProtocol { 53 | public typealias _ObservableTaskFailureType = Swift.Error 54 | public typealias _ResultFailureType = Swift.Error 55 | 56 | public func receive( 57 | _ task: Task 58 | ) async -> Result { 59 | let result = await _performCancellable { 60 | await Result(catching: { () -> Success in 61 | try await task.value 62 | }) 63 | } 64 | 65 | switch result { 66 | case .success(let result): 67 | return result 68 | case .failure(let error): 69 | return .failure(error) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/PublisherExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | @testable import Merge 6 | 7 | import Swallow 8 | import XCTest 9 | 10 | final class PublisherExtensionsTests: XCTestCase { 11 | func testSubscribeAndWait() { 12 | let f1 = Future.async(qos: .unspecified) { () -> Int in 13 | sleep(1) 14 | 15 | return 1 16 | } 17 | 18 | let f2 = Future.async(qos: .unspecified) { () -> Int in 19 | sleep(1) 20 | 21 | return 2 22 | } 23 | 24 | XCTAssert((f1.subscribeAndWaitUntilDone(), f2.subscribeAndWaitUntilDone()) == (1, 2)) 25 | } 26 | 27 | func testEitherPublisher() { 28 | enum TestError: Hashable, Error { 29 | case some 30 | } 31 | 32 | var foo = true 33 | 34 | XCTAssert( 35 | Either { 36 | if foo { 37 | Just("foo").setFailureType(to: TestError.self) 38 | } else { 39 | Fail(error: TestError.some) 40 | } 41 | } 42 | .subscribeAndWaitUntilDone() == .success("foo") 43 | ) 44 | 45 | foo = false 46 | 47 | XCTAssert( 48 | Either { 49 | if foo { 50 | Just("foo").setFailureType(to: TestError.self) 51 | } else { 52 | Fail(error: TestError.some) 53 | } 54 | } 55 | .subscribeAndWaitUntilDone() == .failure(TestError.some) 56 | ) 57 | } 58 | 59 | func testWhilePublisher() { 60 | var count = 0 61 | 62 | Publishers.While(count < 100) { 63 | Just(()).then { 64 | count += 1 65 | } 66 | } 67 | .reduceAndMapTo(()) 68 | .subscribeAndWaitUntilDone() 69 | 70 | XCTAssert(count == 100) 71 | 72 | enum TestError: Hashable, Error { 73 | case some 74 | } 75 | 76 | XCTAssert( 77 | Publishers.While(true) { 78 | Fail(error: TestError.some) 79 | } 80 | .reduceAndMapTo("foo") 81 | .subscribeAndWaitUntilDone() == .failure(TestError.some) 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Mutex/_MutexProtectedType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swallow 7 | 8 | /// A type that is explicitly protected by a mutex. 9 | /// 10 | /// This type is a work-in-progress. Do not use this type directly in your code. 11 | public protocol _MutexProtectedType: Sendable { 12 | associatedtype Mutex: Merge.MutexProtocol 13 | 14 | var mutex: Mutex { get } 15 | } 16 | 17 | extension _MutexProtectedType where Mutex: ScopedMutexProtocol { 18 | public typealias _MutexProtected = Merge.MutexProtected 19 | } 20 | 21 | // MARK: - Extensions 22 | 23 | extension _MutexProtectedType where Mutex: ScopedMutexProtocol { 24 | @discardableResult 25 | public func withMutexProtectedCriticalScope( 26 | _ body: ( 27 | () throws -> T 28 | ) 29 | ) rethrows -> T { 30 | return try mutex.withCriticalScope(body) 31 | } 32 | 33 | @discardableResult 34 | public func withMutexProtectedCriticalScope( 35 | if predicate: @autoclosure () -> Bool, 36 | _ body: (() throws -> T) 37 | ) rethrows -> T? { 38 | return try mutex.withCriticalScope(if: predicate()) { 39 | return try body() 40 | } 41 | } 42 | 43 | @discardableResult 44 | public func withMutexProtectedCriticalScope( 45 | unwrapping value: @autoclosure () -> T?, 46 | _ body: ((T) throws -> U) 47 | ) rethrows -> U? { 48 | return try mutex.withCriticalScope(unwrapping: value(), body) 49 | } 50 | } 51 | 52 | extension _MutexProtectedType where Mutex: ScopedReadWriteMutexProtocol { 53 | @discardableResult 54 | public func withMutexProtectedCriticalScopeForReading( 55 | _ body: ( 56 | () throws -> T 57 | ) 58 | ) rethrows -> T { 59 | return try mutex.withCriticalScopeForReading(body) 60 | } 61 | 62 | @discardableResult 63 | public func withMutexProtectedCriticalScopeForReading( 64 | do expression: @autoclosure () throws -> T 65 | ) rethrows -> T { 66 | return try mutex.withCriticalScopeForReading(expression) 67 | } 68 | 69 | @discardableResult 70 | public func withMutexProtectedCriticalScopeForWriting( 71 | _ body: (() throws -> T) 72 | ) rethrows -> T { 73 | return try mutex.withCriticalScopeForWriting(body) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/AnyObjectWillChangePublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swallow 7 | 8 | public final class AnyObjectWillChangePublisher: Publisher { 9 | public typealias Output = Void 10 | public typealias Failure = Never 11 | 12 | private let base: AnyPublisher 13 | private let _send: () -> Void 14 | 15 | fileprivate init( 16 | publisher: P 17 | ) where P.Failure == Never { 18 | self.base = publisher.mapTo(()).eraseToAnyPublisher() 19 | self._send = { 20 | do { 21 | try cast(publisher, to: _opaque_VoidSender.self).send() 22 | } catch { 23 | runtimeIssue(error) 24 | } 25 | } 26 | } 27 | 28 | public convenience init( 29 | erasing publisher: ObservableObjectPublisher 30 | ) { 31 | self.init(publisher: publisher) 32 | } 33 | 34 | public convenience init( 35 | from object: Object 36 | ) { 37 | self.init(publisher: object.objectWillChange) 38 | } 39 | 40 | public func receive( 41 | subscriber: S 42 | ) where S.Input == Output, S.Failure == Failure { 43 | base.receive(subscriber: subscriber) 44 | } 45 | 46 | public convenience init?(from object: AnyObject) { 47 | let observableObject: (any ObservableObject)? 48 | 49 | if let wrappedValue = object as? (any OptionalProtocol) { 50 | if let _wrappedValue = wrappedValue._wrapped { 51 | observableObject = try! cast(_wrappedValue, to: (any ObservableObject).self) 52 | } else { 53 | observableObject = nil 54 | } 55 | } else { 56 | observableObject = try? cast(object, to: (any ObservableObject).self) 57 | } 58 | 59 | guard let observableObject = observableObject else { 60 | return nil 61 | } 62 | 63 | self.init(from: observableObject) 64 | } 65 | } 66 | 67 | // MARK: - Supplementary - 68 | 69 | extension AnyObjectWillChangePublisher { 70 | public static var empty: AnyObjectWillChangePublisher { 71 | AnyObjectWillChangePublisher(publisher: Empty().eraseToAnyPublisher()) 72 | } 73 | } 74 | 75 | extension AnyObjectWillChangePublisher: _opaque_VoidSender { 76 | public func send() { 77 | _send() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Darwin/DarwinAtomicOperationMemoryOrder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | #if !canImport(PermissionKit) 6 | 7 | import Darwin 8 | import Swallow 9 | 10 | public enum DarwinAtomicOperationMemoryOrder: Hashable { 11 | case relaxed 12 | case consume 13 | case acquire 14 | case release 15 | case acquireRelease 16 | case sequentiallyConsistent 17 | } 18 | 19 | // MARK: - Conformances 20 | 21 | extension DarwinAtomicOperationMemoryOrder: Codable { 22 | public init(from decoder: Decoder) throws { 23 | self.init(rawValue: try RawValue(from: decoder)) 24 | } 25 | 26 | public func encode(to encoder: Encoder) throws { 27 | try rawValue.encode(to: encoder) 28 | } 29 | } 30 | 31 | extension DarwinAtomicOperationMemoryOrder: RawRepresentable { 32 | public typealias RawValue = memory_order 33 | 34 | public var rawValue: memory_order { 35 | switch self { 36 | case .relaxed: 37 | return memory_order_relaxed 38 | case .consume: 39 | return memory_order_consume 40 | case .acquire: 41 | return memory_order_acquire 42 | case .release: 43 | return memory_order_release 44 | case .acquireRelease: 45 | return memory_order_acq_rel 46 | case .sequentiallyConsistent: 47 | return memory_order_seq_cst 48 | } 49 | } 50 | 51 | @inlinable 52 | public init(rawValue: memory_order) { 53 | switch rawValue { 54 | case memory_order_relaxed: 55 | self = .relaxed 56 | case memory_order_consume: 57 | self = .consume 58 | case memory_order_acquire: 59 | self = .acquire 60 | case memory_order_release: 61 | self = .release 62 | case memory_order_acq_rel: 63 | self = .acquireRelease 64 | case memory_order_seq_cst: 65 | self = .sequentiallyConsistent 66 | default: 67 | self = Never.materialize(reason: .impossible) 68 | } 69 | } 70 | } 71 | 72 | // MARK: - Ancillary Conformances - 73 | 74 | extension memory_order: Codable { 75 | public init(from decoder: Decoder) throws { 76 | self.init(rawValue: try UInt32(from: decoder)) 77 | } 78 | 79 | public func encode(to encoder: Encoder) throws { 80 | try rawValue.encode(to: encoder) 81 | } 82 | } 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/SingleOutputPublisher.subscribeAndWaitUntilDone.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Dispatch 6 | import Swallow 7 | 8 | extension SingleOutputPublisher { 9 | /// Synchronously subscribe to the publisher and wait on the current thread until it finishes. 10 | /// 11 | /// This function blocks the calling thread until the publisher emits a completion event. 12 | @discardableResult 13 | public func subscribeAndWaitUntilDone( 14 | on queue: DispatchQueue? = nil 15 | ) -> Result? { 16 | var result: Result? 17 | let queue = queue ?? DispatchQueue(qosClass: .current) 18 | let done = DispatchWorkItem(qos: .unspecified, flags: .inheritQoS, block: {}) 19 | 20 | self 21 | .handleEvents(receiveCancel: { queue.async(execute: done) }) 22 | .subscribe(on: queue) 23 | .receive(on: queue) 24 | .receive( 25 | subscriber: Subscribers.Sink( 26 | receiveCompletion: { completion in 27 | switch completion { 28 | case .finished: 29 | if result == nil { 30 | queue.async(execute: done) 31 | } 32 | case .failure(let error): 33 | result = .failure(error) 34 | 35 | queue.async(execute: done) 36 | } 37 | }, 38 | receiveValue: { value in 39 | result = .success(value) 40 | 41 | queue.async(execute: done) 42 | } 43 | ) 44 | ) 45 | 46 | done.wait() 47 | 48 | return result 49 | } 50 | 51 | /// Synchronously subscribe to the publisher and wait on the current thread until it finishes. 52 | /// 53 | /// This function blocks the calling thread until the publisher emits a completion event. 54 | public func subscribeAndWaitUntilDone() -> Output? where Failure == Never { 55 | guard let result = (subscribeAndWaitUntilDone() as Result?) else { 56 | return nil 57 | } 58 | 59 | guard case .success(let value) = (result as Result) else { 60 | fatalError(.impossible) 61 | } 62 | 63 | return value 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/ObservableTaskOutputPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Foundation 7 | import Swallow 8 | 9 | public struct ObservableTaskOutputPublisher: Publisher { 10 | public typealias Output = TaskOutput 11 | public typealias Failure = ObservableTaskFailure 12 | 13 | private let base: Base 14 | 15 | public init(_ base: Base) { 16 | self.base = base 17 | } 18 | 19 | public func receive( 20 | subscriber: some Subscriber 21 | ) { 22 | defer { 23 | base.start() 24 | } 25 | 26 | guard !base.status.isTerminal else { 27 | if let output = base.status.output { 28 | return Just(output) 29 | .setFailureType(to: Failure.self) 30 | .receive(subscriber: subscriber) 31 | } else if let failure = base.status.failure { 32 | return Fail(error: failure) 33 | .receive(subscriber: subscriber) 34 | } else { 35 | return assertionFailure() 36 | } 37 | } 38 | 39 | base.objectDidChange 40 | .filter({ $0.isTerminal }) 41 | .setFailureType(to: Failure.self) 42 | .flatMap({ status -> AnyPublisher in 43 | if let output = status.output { 44 | return Just(output) 45 | .setFailureType(to: Failure.self) 46 | .eraseToAnyPublisher() 47 | } else if let failure = status.failure { 48 | return Fail(error: failure) 49 | .eraseToAnyPublisher() 50 | } else { 51 | assertionFailure() 52 | 53 | return Fail(error: .canceled) 54 | .eraseToAnyPublisher() 55 | } 56 | }) 57 | .handleCancel { 58 | guard base.status.isTerminal else { 59 | runtimeIssue(CancellationError()) 60 | 61 | return 62 | } 63 | } 64 | .receive(subscriber: subscriber) 65 | } 66 | } 67 | 68 | // MARK: - API 69 | 70 | extension ObservableTask { 71 | public var outputPublisher: ObservableTaskOutputPublisher { 72 | ObservableTaskOutputPublisher(self) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Merge 2 | 3 | Robust task management and concurrency utilities built atop Combine. 4 | 5 | # Usage 6 | 7 | ### `ObservableTask` 8 | 9 | `ObservableTask` is one of the main exports of this framework. 10 | 11 | ```swift 12 | /// An observable task is a token of activity with status-reporting. 13 | public protocol ObservableTask: Identifiable, ObservableObject where 14 | ObjectWillChangePublisher.Output == TaskStatus { 15 | associatedtype Success 16 | associatedtype Error: Swift.Error 17 | 18 | /// The status of this task. 19 | var status: TaskStatus { get } 20 | 21 | /// The progress of the this task. 22 | var progress: Progress { get } 23 | 24 | /// Start the task. 25 | func start() 26 | 27 | /// Pause the task. 28 | func pause() throws 29 | 30 | /// Resume the task. 31 | func resume() throws 32 | 33 | /// Cancel the task. 34 | func cancel() 35 | } 36 | 37 | ``` 38 | 39 | An observable task can be thought of a status-reporting publisher subscription. 40 | 41 | ### `@PublishedObject` 42 | 43 | `@PublishedObject` is a property wrapper extends the capabilities of the `@Published` property wrapper to instances that conform to the `ObservableObject` protocol. 44 | 45 | This allows for automatic observation and reaction to changes within objects that are marked as observable. 46 | 47 | ```swift 48 | // a class that comforms to the ObservableObject protocol 49 | class MyObject: ObservableObject { 50 | @Published var someText: String 51 | 52 | init(someText: String) { 53 | self.someText = someText 54 | } 55 | } 56 | 57 | struct MyObjectViewModel { 58 | @PublishedObject var currentObject: MyObject? = nil 59 | @PublishedObject var objects: [MyObject] = [] 60 | } 61 | ``` 62 | 63 | # Motivation 64 | 65 | While **Combine** publishers are great, they're not suited for complex task management for the following reasons: 66 | 67 | - Publishers fundamentally lack the concept of progress reporting. Publishers aren't 'live' reactive streams, they *create* live streams upon initiating a new subscription but these streams are opaque (beyond the support of backpressure/cancellation). 68 | - Publishers support the concept of backpressure, but do not support hard/well-defined concepts of pause/resume. This may be favorable in design for a general-purpose reactive programming framework, but a deficit for constructing complex task graphs that need to unambiguously support pause/resume. 69 | 70 | `ObservableTask` was created to fill the need for a modern-day `NSOperation` replacement. 71 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observable Tasks/Status/ObservableTaskFailure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Diagnostics 7 | import Swallow 8 | 9 | protocol ObservableTaskFailureProtocol { 10 | var _opaque_error: (any Swift.Error)? { get } 11 | } 12 | 13 | extension ObservableTaskFailure: ObservableTaskFailureProtocol { 14 | public var _opaque_error: (any Swift.Error)? { 15 | switch self { 16 | case .canceled: 17 | return nil 18 | case .error(let error): 19 | return error 20 | } 21 | } 22 | } 23 | 24 | /// An enumeration that represents the source of task failure. 25 | @frozen 26 | public enum ObservableTaskFailure: _ErrorX, HashEquatable { 27 | case canceled 28 | case error(Error) 29 | 30 | public var traits: ErrorTraits { 31 | switch self { 32 | case .canceled: 33 | assertionFailure() 34 | 35 | return [] 36 | case .error(let error): 37 | return AnyError(erasing: error).traits 38 | } 39 | } 40 | 41 | public init?(_catchAll error: AnyError) throws { 42 | guard let _error = try cast(Error.self, to: (any _ErrorX.Type).self).init(_catchAll: error) else { 43 | return nil 44 | } 45 | 46 | self = try .error(cast(_error)) 47 | } 48 | 49 | public func hash(into hasher: inout Hasher) { 50 | switch self { 51 | case .canceled: 52 | hasher.combine(AnyError(erasing: CancellationError())) 53 | case .error(let error): 54 | hasher.combine(AnyError(erasing: error)) 55 | } 56 | } 57 | } 58 | 59 | // MARK: - Initializers 60 | 61 | extension ObservableTaskFailure { 62 | public init?(_ status: ObservableTaskStatus) { 63 | if let failure = status.failure { 64 | self = failure 65 | } else { 66 | return nil 67 | } 68 | } 69 | } 70 | 71 | // MARK: - Supplementary 72 | 73 | extension AnyError { 74 | public init(from failure: ObservableTaskFailure) { 75 | switch failure { 76 | case .canceled: 77 | self.init(erasing: CancellationError()) 78 | case .error(let error): 79 | self.init(erasing: error) 80 | } 81 | } 82 | } 83 | 84 | extension Subscribers.Completion { 85 | public static func failure( 86 | _ error: Error 87 | ) -> Self where Failure == ObservableTaskFailure { 88 | .failure(.error(error)) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Combine/Publisher+Task.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | 7 | extension Publisher { 8 | public func flatMapAsync( 9 | _ transform: @escaping (Output) async -> T 10 | ) -> Publishers.FlatMap, Self> { 11 | flatMap { value in 12 | Future { promise in 13 | Task { 14 | let result = await transform(value) 15 | promise(.success(result)) 16 | } 17 | } 18 | } 19 | } 20 | 21 | public func flatMapAsync( 22 | _ transform: @escaping (Output) async throws -> T 23 | ) -> Publishers.FlatMap, Publishers.MapError> { 24 | mapError({ $0 as Error }).flatMap { value in 25 | Future { promise in 26 | Task { 27 | do { 28 | let result = try await transform(value) 29 | 30 | promise(.success(result)) 31 | } catch { 32 | promise(.failure(error)) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | public func flatMapAsyncSequential( 40 | _ transform: @escaping (Output) async -> T 41 | ) -> Publishers.FlatMap, Publishers.SetFailureType> { 42 | let queue = TaskQueue() 43 | 44 | let x = flatMap { value in 45 | Future { fulfill in 46 | queue.addTask { 47 | let result = await Result(catching: { 48 | await transform(value) 49 | }) 50 | 51 | fulfill(result) 52 | } 53 | } 54 | } 55 | 56 | return x 57 | } 58 | 59 | public func flatMapAsyncSequential( 60 | _ transform: @escaping (Output) async throws -> T 61 | ) -> Publishers.FlatMap, any Error>, Publishers.MapError> { 62 | let queue = ThrowingTaskQueue() 63 | 64 | let x = flatMap { value in 65 | Future { fulfill in 66 | queue.addTask { 67 | let result = await Result(catching: { 68 | try await transform(value) 69 | }) 70 | 71 | fulfill(result) 72 | } 73 | } 74 | } 75 | 76 | return x 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Process/_AsyncProcess+Initializers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Diagnostics 7 | import Foundation 8 | @_spi(Internal) import Swallow 9 | import System 10 | 11 | // MARK: - Initializers 12 | 13 | #if os(macOS) || targetEnvironment(macCatalyst) 14 | @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) 15 | @available(macCatalyst, unavailable) 16 | extension _AsyncProcess { 17 | public convenience init( 18 | executableURL: URL?, 19 | arguments: [String], 20 | currentDirectoryURL: URL? = nil, 21 | environmentVariables: [String: String] = [:], 22 | options: [_AsyncProcess.Option]? 23 | ) throws { 24 | #if os(macOS) 25 | try self.init( 26 | existingProcess: nil, 27 | options: options 28 | ) 29 | 30 | self.process.executableURL = executableURL ?? URL(fileURLWithPath: "/bin/zsh") 31 | self.process.arguments = arguments 32 | self.process.currentDirectoryURL = currentDirectoryURL?._fromURLToFileURL() 33 | self.process.environment = environmentVariables 34 | #else 35 | fatalError(.unsupported) 36 | #endif 37 | } 38 | 39 | public convenience init( 40 | launchPath: String?, 41 | arguments: [String], 42 | currentDirectoryURL: URL? = nil, 43 | environmentVariables: [String: String] = [:], 44 | options: [_AsyncProcess.Option]? 45 | ) throws { 46 | try self.init( 47 | executableURL: launchPath.map({ URL(fileURLWithPath: $0) }), 48 | arguments: arguments, 49 | currentDirectoryURL: currentDirectoryURL, 50 | environmentVariables: environmentVariables, 51 | options: options 52 | ) 53 | } 54 | } 55 | #else 56 | @available(macOS 11.0, *) 57 | @available(iOS, unavailable) 58 | @available(macCatalyst, unavailable) 59 | @available(tvOS, unavailable) 60 | @available(watchOS, unavailable) 61 | extension _AsyncProcess { 62 | public convenience init( 63 | executableURL: URL?, 64 | arguments: [String], 65 | currentDirectoryURL: URL? = nil, 66 | environmentVariables: [String: String] = [:], 67 | options: [_AsyncProcess.Option]? 68 | ) throws { 69 | fatalError(.unavailable) 70 | } 71 | 72 | public convenience init( 73 | launchPath: String?, 74 | arguments: [String], 75 | currentDirectoryURL: URL? = nil, 76 | environmentVariables: [String: String] = [:], 77 | options: [_AsyncProcess.Option]? 78 | ) throws { 79 | fatalError(.unavailable) 80 | } 81 | } 82 | #endif 83 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Extensions/Combine/_CombineAsyncStream.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | public class _CombineAsyncStream: AsyncSequence { 9 | public typealias Element = Upstream.Output 10 | public typealias AsyncIterator = _CombineAsyncStream 11 | 12 | private var stream: AsyncThrowingStream 13 | private var cancellable: AnyCancellable? 14 | private lazy var iterator = stream.makeAsyncIterator() 15 | 16 | public init(_ upstream: Upstream) { 17 | stream = .init { _ in } 18 | cancellable = nil 19 | stream = .init { continuation in 20 | continuation.onTermination = { [weak self] _ in 21 | self?.cancellable?.cancel() 22 | } 23 | 24 | cancellable = 25 | upstream 26 | .handleEvents( 27 | receiveCancel: { [weak self] in 28 | continuation.finish(throwing: nil) 29 | self?.cancellable = nil 30 | } 31 | ) 32 | .sink( 33 | receiveCompletion: { [weak self] completion in 34 | switch completion { 35 | case .failure(let error): 36 | continuation.finish(throwing: error) 37 | case .finished: 38 | continuation.finish(throwing: nil) 39 | } 40 | self?.cancellable = nil 41 | }, 42 | receiveValue: { value in 43 | continuation.yield(value) 44 | }) 45 | } 46 | } 47 | 48 | public func makeAsyncIterator() -> Self { 49 | return self 50 | } 51 | } 52 | 53 | extension _CombineAsyncStream: AsyncIteratorProtocol { 54 | public func next() async throws -> Upstream.Output? { 55 | return try await iterator.next() 56 | } 57 | } 58 | 59 | extension Publisher { 60 | public func toAsyncStream() -> AsyncStream where Failure == Never { 61 | if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) { 62 | return 63 | self 64 | .buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest) 65 | .values 66 | .eraseToStream() 67 | } else { 68 | return _CombineAsyncStream(self).eraseToStream() 69 | } 70 | } 71 | 72 | public func toAsyncThrowingStream() -> AsyncThrowingStream where Failure == Error { 73 | _CombineAsyncStream(self).eraseToThrowingStream() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/Combine/MainThreadScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Dispatch 7 | import Foundation 8 | import Swallow 9 | 10 | public struct MainThreadScheduler: Scheduler, @unchecked Sendable { 11 | public typealias SchedulerTimeType = DispatchQueue.SchedulerTimeType 12 | public typealias SchedulerOptions = DispatchQueue.SchedulerOptions 13 | 14 | public static var shared: Self { 15 | Self() 16 | } 17 | 18 | @usableFromInline 19 | let base = DispatchQueue.main 20 | 21 | private init() { 22 | 23 | } 24 | 25 | public var now: SchedulerTimeType { 26 | base.now 27 | } 28 | 29 | public var minimumTolerance: SchedulerTimeType.Stride { 30 | base.minimumTolerance 31 | } 32 | 33 | @_transparent 34 | public func schedule( 35 | _ action: @escaping () -> Void 36 | ) { 37 | if Thread.isMainThread { 38 | action() 39 | } else { 40 | base.schedule(options: nil, action) 41 | } 42 | } 43 | 44 | @_transparent 45 | public func schedule( 46 | options: SchedulerOptions?, 47 | _ action: @escaping () -> Void 48 | ) { 49 | if Thread.isMainThread { 50 | action() 51 | } else { 52 | base.schedule(options: options, action) 53 | } 54 | } 55 | 56 | public func schedule( 57 | after date: SchedulerTimeType, 58 | tolerance: SchedulerTimeType.Stride, 59 | options: SchedulerOptions?, 60 | _ action: @escaping () -> Void 61 | ) { 62 | base.schedule( 63 | after: date, 64 | tolerance: tolerance, 65 | options: options, 66 | action 67 | ) 68 | } 69 | 70 | /// Performs the action at some time after the specified date, at the specified frequency, optionally taking into account tolerance if possible. 71 | public func schedule( 72 | after date: SchedulerTimeType, 73 | interval: SchedulerTimeType.Stride, 74 | tolerance: SchedulerTimeType.Stride, 75 | options: SchedulerOptions?, 76 | _ action: @escaping () -> Void 77 | ) -> Cancellable { 78 | base.schedule( 79 | after: date, 80 | interval: interval, 81 | tolerance: tolerance, 82 | options: options, 83 | action 84 | ) 85 | } 86 | } 87 | 88 | extension Scheduler where Self == MainThreadScheduler { 89 | public static var mainThread: Self { 90 | .shared 91 | } 92 | } 93 | 94 | extension Publisher { 95 | public func receiveOnMainThread() -> Publishers.ReceiveOn { 96 | receive(on: MainThreadScheduler.shared) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Rate-limiting & Retrying/_TaskRetryPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swallow 7 | 8 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 9 | public enum _TaskRetryStrategy: Sendable { 10 | case delay(any TaskRetryDelayStrategy, initial: Duration) 11 | 12 | public static func delay( 13 | _ strategy: some TaskRetryDelayStrategy 14 | ) -> Self { 15 | .delay(strategy, initial: .seconds(1)) 16 | } 17 | 18 | public static func delay( 19 | duration: Duration 20 | ) -> Self { 21 | .delay(.linear, initial: .seconds(1)) 22 | } 23 | } 24 | 25 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 26 | public struct _TaskRetryPolicy: Sendable { 27 | public var strategy: _TaskRetryStrategy 28 | public let maxRetryCount: Int? 29 | public let onFailure: @Sendable (Error, Int) throws -> () 30 | 31 | public init( 32 | strategy: _TaskRetryStrategy, 33 | maxRetryCount: Int? = 1, 34 | onFailure: @escaping @Sendable (Error, Int) throws -> () = { _, _ in } 35 | ) { 36 | self.strategy = strategy 37 | self.maxRetryCount = maxRetryCount 38 | self.onFailure = onFailure 39 | } 40 | } 41 | 42 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 43 | public enum _TaskRetryError: Error { 44 | case maximumRetriesExceeded(Int) 45 | } 46 | 47 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 48 | public func withTaskRetryPolicy( 49 | _ retryPolicy: _TaskRetryPolicy?, 50 | operation: @Sendable () async throws -> Result 51 | ) async throws -> Result { 52 | guard let retryPolicy else { 53 | return try await operation() 54 | } 55 | 56 | switch retryPolicy.strategy { 57 | case .delay(let delay, let initial): 58 | do { 59 | var attempt = 1 60 | 61 | while true { 62 | try Task.checkCancellation() 63 | 64 | do { 65 | return try await operation() 66 | } catch { 67 | try retryPolicy.onFailure(error, attempt) 68 | } 69 | 70 | if let maxRetryCount = retryPolicy.maxRetryCount, (attempt - 1) > maxRetryCount { 71 | throw _TaskRetryError.maximumRetriesExceeded(maxRetryCount) 72 | } 73 | 74 | let delay = delay.delay(forAttempt: attempt, withInitialDelay: initial) 75 | 76 | try await Task.sleep(for: delay) 77 | 78 | attempt += 1 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observation/_withContinuousObservationTracking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | #if canImport(Observation) 7 | import Observation 8 | #endif 9 | import Swallow 10 | 11 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 12 | public func _withContinuousObservationTracking( 13 | applying block: @escaping () -> Void, 14 | onChange: @autoclosure () -> @Sendable () -> Void, 15 | isolation: isolated (any Actor)? = #isolation 16 | ) -> _ContinuousObservationTrackingSubscription { 17 | let isCancelled = Swallow._OSUnfairLocked.init(initialState: false) 18 | let _onChange: @Sendable () -> Void = onChange() 19 | 20 | __internal_withContinuousObservationTracking( 21 | applying: block, 22 | isCancelled: isCancelled, 23 | onChange: _onChange 24 | ) 25 | 26 | return _ContinuousObservationTrackingSubscription { 27 | isCancelled.withLock({ $0 = true }) 28 | } 29 | } 30 | 31 | public func _withContinuousObservationTrackingIfAvailable( 32 | applying block: @escaping () -> Void, 33 | onChange: @autoclosure () -> @Sendable () -> Void, 34 | isolation: isolated (any Actor)? = #isolation 35 | ) -> _ContinuousObservationTrackingSubscription { 36 | if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { 37 | return _withContinuousObservationTracking( 38 | applying: block, 39 | onChange: onChange(), 40 | isolation: isolation 41 | ) 42 | } else { 43 | return _ContinuousObservationTrackingSubscription(onCancel: {}) 44 | } 45 | } 46 | 47 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 48 | private func __internal_withContinuousObservationTracking( 49 | applying block: @escaping () -> Void, 50 | isCancelled: Swallow._OSUnfairLocked, 51 | onChange: @escaping @Sendable () -> Void, 52 | isolation: isolated (any Actor)? = #isolation 53 | ) { 54 | let box = _UncheckedSendable(block) 55 | 56 | withObservationTracking { 57 | block() 58 | } onChange: { 59 | onChange() 60 | 61 | guard isCancelled.withCriticalScope({ !$0 }) else { 62 | return 63 | } 64 | 65 | Task { 66 | await __internal_withContinuousObservationTracking( 67 | applying: box.wrappedValue, 68 | isCancelled: isCancelled, 69 | onChange: onChange, 70 | isolation: isolation 71 | ) 72 | } 73 | } 74 | } 75 | 76 | // MARK: - Auxiliary 77 | 78 | public struct _ContinuousObservationTrackingSubscription: Sendable, Cancellable { 79 | private let onCancel: @Sendable () -> Void 80 | 81 | init(onCancel: @escaping @Sendable () -> Void) { 82 | self.onCancel = onCancel 83 | } 84 | 85 | public func cancel() { 86 | onCancel() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/_Concurrency/_offTheMainThread.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Foundation 6 | import Swallow 7 | 8 | extension Task { 9 | @discardableResult 10 | public static func _offTheMainThread( 11 | priority: TaskPriority? = nil, 12 | operation: @escaping () async -> Success 13 | ) -> Self where Failure == Never { 14 | if Thread._isMainThread { 15 | return Task.detached(priority: priority) { 16 | assert(!Thread._isMainThread) 17 | 18 | return await operation() 19 | } 20 | } else { 21 | return Task(priority: priority) { 22 | if Thread._isMainThread { 23 | let task = await MainActor.run { 24 | Task.detached(priority: priority) { 25 | assert(!Thread._isMainThread) 26 | 27 | return await operation() 28 | } 29 | } 30 | 31 | return await task.value 32 | } else { 33 | return await operation() 34 | } 35 | } 36 | } 37 | } 38 | 39 | @discardableResult 40 | public static func _offTheMainThread( 41 | priority: TaskPriority? = nil, 42 | operation: @escaping () async throws -> Success 43 | ) -> Self where Failure == Swift.Error { 44 | if Thread._isMainThread { 45 | return Task.detached(priority: priority) { 46 | assert(!Thread._isMainThread) 47 | 48 | return try await operation() 49 | } 50 | } else { 51 | return Task(priority: priority) { 52 | if Thread._isMainThread { 53 | let task = await MainActor.run { 54 | Task.detached(priority: priority) { 55 | assert(!Thread._isMainThread) 56 | 57 | return try await operation() 58 | } 59 | } 60 | 61 | return try await task.value 62 | } else { 63 | return try await operation() 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | public func _offTheMainThread( 71 | priority: TaskPriority? = nil, 72 | operation: @escaping () async -> Success 73 | ) async -> Success { 74 | await Task._offTheMainThread(priority: priority, operation: operation).value 75 | } 76 | 77 | public func _offTheMainThread( 78 | priority: TaskPriority? = nil, 79 | operation: @escaping () async throws -> Success 80 | ) async throws -> Success { 81 | try await Task._offTheMainThread(priority: priority, operation: operation).value 82 | } 83 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "maximumBlankLines": 1000, 4 | "lineLength": 100000, 5 | "tabWidth": 8, 6 | "indentation": { 7 | "spaces": 4 8 | }, 9 | "spacesBeforeEndOfLineComments": 2, 10 | "respectsExistingLineBreaks": true, 11 | "lineBreakBeforeControlFlowKeywords": false, 12 | "lineBreakBeforeEachArgument": false, 13 | "lineBreakBeforeEachGenericRequirement": false, 14 | "lineBreakBetweenDeclarationAttributes": false, 15 | "prioritizeKeepingFunctionOutputTogether": true, 16 | "indentConditionalCompilationBlocks": false, 17 | "lineBreakAroundMultilineExpressionChainComponents": false, 18 | "indentSwitchCaseLabels": true, 19 | "spacesAroundRangeFormationOperators": false, 20 | "multiElementCollectionTrailingCommas": false, 21 | "reflowMultilineStringLiterals": { 22 | "never": {} 23 | }, 24 | "indentBlankLines": true, 25 | "fileScopedDeclarationPrivacy": { 26 | "accessLevel": "private" 27 | }, 28 | "noAssignmentInExpressions": { 29 | "allowedFunctions": ["XCTAssertNoThrow"] 30 | }, 31 | "rules": { 32 | "AllPublicDeclarationsHaveDocumentation": false, 33 | "AlwaysUseLiteralForEmptyCollectionInit": false, 34 | "AlwaysUseLowerCamelCase": false, 35 | "AmbiguousTrailingClosureOverload": false, 36 | "AvoidRetroactiveConformances": false, 37 | "BeginDocumentationCommentWithOneLineSummary": false, 38 | "DoNotUseSemicolons": false, 39 | "DontRepeatTypeInStaticProperties": false, 40 | "FileScopedDeclarationPrivacy": false, 41 | "FullyIndirectEnum": false, 42 | "GroupNumericLiterals": false, 43 | "IdentifiersMustBeASCII": false, 44 | "NeverForceUnwrap": false, 45 | "NeverUseForceTry": false, 46 | "NeverUseImplicitlyUnwrappedOptionals": false, 47 | "NoAccessLevelOnExtensionDeclaration": false, 48 | "NoAssignmentInExpressions": false, 49 | "NoBlockComments": false, 50 | "NoCasesWithOnlyFallthrough": false, 51 | "NoEmptyLinesOpeningClosingBraces": false, 52 | "NoEmptyTrailingClosureParentheses": false, 53 | "NoLabelsInCasePatterns": false, 54 | "NoLeadingUnderscores": false, 55 | "NoParensAroundConditions": false, 56 | "NoPlaygroundLiterals": false, 57 | "NoVoidReturnOnFunctionSignature": false, 58 | "OmitExplicitReturns": false, 59 | "OneCasePerLine": false, 60 | "OneVariableDeclarationPerLine": false, 61 | "OnlyOneTrailingClosureArgument": false, 62 | "OrderedImports": false, 63 | "ReplaceForEachWithForLoop": false, 64 | "ReturnVoidInsteadOfEmptyTuple": false, 65 | "TypeNamesShouldBeCapitalized": false, 66 | "UseEarlyExits": false, 67 | "UseExplicitNilCheckInConditions": false, 68 | "UseLetInEveryBoundCaseVariable": false, 69 | "UseShorthandTypeNames": false, 70 | "UseSingleLinePropertyGetter": false, 71 | "UseSynthesizedInitializer": false, 72 | "UseTripleSlashForDocumentationComments": false, 73 | "UseWhereClausesInForLoops": false, 74 | "ValidateDocumentationComments": false 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Observation/_RuntimeConditionalObservationTrackedValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | #if canImport(Observation) 7 | import Observation 8 | #endif 9 | import Swallow 10 | 11 | @propertyWrapper 12 | public final class _RuntimeConditionalObservationTrackedValue: _ObservationRegistrarNotifying { 13 | private var wrappedValueBox: AnyMutablePropertyWrapper 14 | private var _observationRegistrarNotifier: any _ObservationRegistrarNotifying 15 | 16 | public var wrappedValue: T { 17 | get { 18 | wrappedValueBox.wrappedValue 19 | } 20 | set { 21 | wrappedValueBox.wrappedValue = newValue 22 | } 23 | } 24 | 25 | public init(wrappedValue: T) { 26 | if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { 27 | let valueBox = _PassthroughObservationTrackedValue(wrappedValue: wrappedValue) 28 | 29 | self.wrappedValueBox = AnyMutablePropertyWrapper(valueBox) 30 | self._observationRegistrarNotifier = valueBox 31 | } else { 32 | self.wrappedValueBox = AnyMutablePropertyWrapper(ReferenceBox(wrappedValue)) 33 | self._observationRegistrarNotifier = _DummyObservationRegistrarNotifying() 34 | } 35 | } 36 | 37 | public func notifyingObservationRegistrar( 38 | _ kind: _ObservationRegistrarTrackedOperationKind, 39 | perform operation: () -> Result 40 | ) -> Result { 41 | _observationRegistrarNotifier.notifyingObservationRegistrar(kind, perform: operation) 42 | } 43 | } 44 | 45 | // MARK: - Auxiliary 46 | 47 | @propertyWrapper 48 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 49 | @Observable 50 | public final class _PassthroughObservationTrackedValue: MutablePropertyWrapper, _ObservationRegistrarNotifying { 51 | public var wrappedValue: T 52 | 53 | public init(wrappedValue: T) { 54 | self.wrappedValue = wrappedValue 55 | } 56 | 57 | public func notifyingObservationRegistrar( 58 | _ kind: _ObservationRegistrarTrackedOperationKind, 59 | perform operation: () -> Result 60 | ) -> Result { 61 | access(keyPath: \.wrappedValue) 62 | 63 | if kind == .mutation { 64 | _$observationRegistrar.willSet(self, keyPath: \.wrappedValue) 65 | } 66 | 67 | let result: Result = operation() 68 | 69 | if kind == .mutation { 70 | _$observationRegistrar.didSet(self, keyPath: \.wrappedValue) 71 | } 72 | 73 | return result 74 | } 75 | } 76 | 77 | public struct _DummyObservationRegistrarNotifying: _ObservationRegistrarNotifying { 78 | public func notifyingObservationRegistrar( 79 | _ kind: _ObservationRegistrarTrackedOperationKind, 80 | perform operation: () -> Result 81 | ) -> Result { 82 | return operation() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Process/_AsyncProcess._StandardStreamsBuffer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import FoundationX 6 | import Swallow 7 | 8 | @available(macOS 11.0, *) 9 | @available(iOS, unavailable) 10 | @available(macCatalyst, unavailable) 11 | @available(tvOS, unavailable) 12 | @available(watchOS, unavailable) 13 | public actor _StandardInputOutputStreamsBuffer { 14 | private var standardOutputBuffer: Data = Data() 15 | private var standardErrorBuffer: Data = Data() 16 | private let publishers: _AsyncProcess._Publishers 17 | private let options: Set<_AsyncProcess.Option> 18 | 19 | init(publishers: _AsyncProcess._Publishers, options: Set<_AsyncProcess.Option>) { 20 | self.publishers = publishers 21 | self.options = options 22 | } 23 | 24 | func record( 25 | data: Data, 26 | forPipe pipe: Pipe, 27 | pipeName: _ProcessPipeName 28 | ) async { 29 | guard !data.isEmpty else { 30 | return 31 | } 32 | 33 | switch pipeName { 34 | case .standardOutput: 35 | publishers.standardOutputPublisher.send(data) 36 | case .standardError: 37 | publishers.standardErrorPublisher.send(data) 38 | default: 39 | break 40 | } 41 | 42 | _record(data: data, forPipe: pipeName) 43 | 44 | await forwardToTerminalIfNecessary(data: data, forPipe: pipeName) 45 | } 46 | 47 | private func forwardToTerminalIfNecessary( 48 | data: Data, 49 | forPipe pipe: _ProcessPipeName 50 | ) async { 51 | let forwardStdoutStderrToTerminal: Bool = options.contains(where: { $0._stdoutStderrSink == .terminal }) // FIXME: (@vmanot) unhandled cases 52 | 53 | if forwardStdoutStderrToTerminal { 54 | switch pipe { 55 | case .standardOutput: 56 | FileHandle.standardOutput.write(data) 57 | case .standardError: 58 | FileHandle.standardError.write(data) 59 | default: 60 | break 61 | } 62 | } 63 | } 64 | 65 | private func _record( 66 | data: Data, 67 | forPipe pipe: _ProcessPipeName 68 | ) { 69 | assert(!data.isEmpty) 70 | 71 | switch pipe { 72 | case .standardOutput: 73 | standardOutputBuffer += data 74 | case .standardError: 75 | standardErrorBuffer += data 76 | case .standardInput: 77 | assertionFailure() 78 | 79 | break 80 | } 81 | } 82 | 83 | func _standardOutputStringUsingUTF8() throws -> String { 84 | try standardOutputBuffer.toString(encoding: .utf8) 85 | } 86 | 87 | func _standardErrorStringUsingUTF8() throws -> String { 88 | try standardErrorBuffer.toString(encoding: .utf8) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Merge/Intermodular/Helpers/_Concurrency/Task.repeat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Dispatch 7 | import Foundation 8 | import Swift 9 | 10 | extension Task where Success == Void, Failure == Error { 11 | /// Runs the given asynchronous operation repeatedly while a given predicate evaluates to `true`. 12 | @discardableResult 13 | public static func `repeat`( 14 | while predicate: @escaping () throws -> Bool, 15 | maxRepetitions: Int = Int.max, 16 | _ operation: @escaping () async throws -> Void 17 | ) -> Task { 18 | Task { 19 | var numberOfRepetitions: Int = 0 20 | 21 | while try numberOfRepetitions <= maxRepetitions && (try predicate()) { 22 | try await operation() 23 | 24 | numberOfRepetitions += 1 25 | } 26 | } 27 | } 28 | 29 | /// Runs the given asynchronous operation repeatedly while a given predicate evaluates to `true`. 30 | @discardableResult 31 | public static func `repeat`( 32 | while predicate: @escaping () async throws -> Bool, 33 | maxRepetitions: Int = Int.max, 34 | _ operation: @escaping () async throws -> Void 35 | ) -> Task { 36 | Task { 37 | var numberOfRepetitions: Int = 0 38 | 39 | while numberOfRepetitions <= maxRepetitions { 40 | let shouldContinue = try await predicate() 41 | 42 | guard shouldContinue else { 43 | break 44 | } 45 | 46 | try await operation() 47 | 48 | numberOfRepetitions += 1 49 | } 50 | } 51 | } 52 | } 53 | 54 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) 55 | extension Task where Success == Void, Failure == Error { 56 | /// Runs the given asynchronous operation repeatedly on the given interval on behalf of the current actor. 57 | @discardableResult 58 | public static func `repeat`( 59 | every interval: DispatchTimeInterval, 60 | on runLoop: RunLoop = .main, 61 | operation: @escaping () async throws -> Void 62 | ) -> Task { 63 | let _runLoop = _UncheckedSendable(wrappedValue: runLoop) 64 | 65 | return _Concurrency.Task { 66 | guard let interval = try? interval.toTimeInterval() else { 67 | assertionFailure("unsupported interval: \(interval)") 68 | 69 | return 70 | } 71 | 72 | try _Concurrency.Task.checkCancellation() 73 | 74 | for await _ in Timer.publish(every: interval, on: _runLoop.wrappedValue, in: .default).autoconnect().values { 75 | try _Concurrency.Task.checkCancellation() 76 | 77 | try await operation() 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.1 2 | 3 | import PackageDescription 4 | 5 | var package = Package( 6 | name: "Merge", 7 | platforms: [ 8 | .iOS(.v13), 9 | .macOS(.v13), 10 | .tvOS(.v13), 11 | .watchOS(.v6), 12 | ], 13 | products: [ 14 | .library( 15 | name: "Merge", 16 | targets: [ 17 | "CommandLineToolSupport", 18 | "ShellScripting", 19 | "SwiftDI", 20 | "Merge", 21 | ], 22 | ) 23 | ], 24 | dependencies: [ 25 | .package(url: "https://github.com/vmanot/Swallow.git", branch: "master"), 26 | .package(url: "https://github.com/preternatural-fork/swift-subprocess.git", branch: "release/0.2.1") 27 | ], 28 | targets: [ 29 | .target( 30 | name: "SwiftDI", 31 | dependencies: [ 32 | "Swallow" 33 | ], 34 | path: "Sources/SwiftDI", 35 | swiftSettings: [ 36 | .enableExperimentalFeature("AccessLevelOnImport"), 37 | .swiftLanguageMode(.v5), 38 | ] 39 | ), 40 | .target( 41 | name: "Merge", 42 | dependencies: [ 43 | "Swallow", 44 | .product(name: "SwallowMacrosClient", package: "Swallow"), 45 | "SwiftDI", 46 | ], 47 | path: "Sources/Merge", 48 | swiftSettings: [ 49 | .enableExperimentalFeature("AccessLevelOnImport"), 50 | .swiftLanguageMode(.v5), 51 | ] 52 | ), 53 | .target( 54 | name: "ShellScripting", 55 | dependencies: [ 56 | "Merge", 57 | .product( 58 | name: "Subprocess", 59 | package: "swift-subprocess", 60 | condition: .when(platforms: [.macOS]) 61 | ), 62 | ], 63 | path: "Sources/ShellScripting", 64 | swiftSettings: [ 65 | .enableExperimentalFeature("AccessLevelOnImport"), 66 | .swiftLanguageMode(.v5), 67 | ] 68 | ), 69 | .target( 70 | name: "CommandLineToolSupport", 71 | dependencies: [ 72 | "Merge", 73 | "ShellScripting", 74 | "Swallow", 75 | ], 76 | path: "Sources/CommandLineToolSupport", 77 | swiftSettings: [ 78 | .enableExperimentalFeature("AccessLevelOnImport"), 79 | .swiftLanguageMode(.v5), 80 | ] 81 | ), 82 | .testTarget( 83 | name: "MergeTests", 84 | dependencies: [ 85 | "CommandLineToolSupport", 86 | "Merge", 87 | "ShellScripting", 88 | ], 89 | path: "Tests", 90 | swiftSettings: [ 91 | .swiftLanguageMode(.v5) 92 | ] 93 | ), 94 | ], 95 | swiftLanguageModes: [.v5] 96 | ) 97 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/_MultiReaderSubject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import FoundationX 7 | import Swallow 8 | 9 | public final class _MultiReaderSubject { 10 | private let base = PassthroughSubject<(sender: Child.ID, payload: Result), Never>() 11 | private let rootID = Child.ID() 12 | 13 | public init() { 14 | 15 | } 16 | 17 | public func child() -> Child { 18 | .init(parent: self, id: .init()) 19 | } 20 | } 21 | 22 | extension _MultiReaderSubject: Publisher { 23 | public func receive(subscriber: S) where S.Input == Output, S.Failure == Failure { 24 | let rootID = self.rootID 25 | 26 | base 27 | .filter({ $0.sender != rootID }) 28 | .tryMap({ try $0.payload.get() }) 29 | .mapError({ $0 as! Failure }) 30 | .receive(subscriber: subscriber) 31 | } 32 | } 33 | 34 | extension _MultiReaderSubject: Subject { 35 | public func send(_ value: Output) { 36 | base.send((rootID, .success(value))) 37 | } 38 | 39 | public func send(completion: Subscribers.Completion) { 40 | switch completion { 41 | case .finished: 42 | base.send(completion: .finished) 43 | case .failure(let error): 44 | base.send((rootID, .failure(error))) 45 | } 46 | } 47 | 48 | public func send(subscription: Subscription) { 49 | base.send(subscription: subscription) 50 | } 51 | } 52 | 53 | extension _MultiReaderSubject { 54 | public final class Child: Publisher, Subject { 55 | public typealias ID = _AutoIncrementingIdentifier 56 | 57 | private let parent: _MultiReaderSubject 58 | private let id: ID 59 | 60 | fileprivate init(parent: _MultiReaderSubject, id: ID) { 61 | self.parent = parent 62 | self.id = id 63 | } 64 | 65 | public func receive(subscriber: S) where S.Input == Output, S.Failure == Failure { 66 | let id = self.id 67 | 68 | parent.base 69 | .filter({ $0.sender != id }) 70 | .tryMap({ try $0.payload.get() }) 71 | .mapError({ $0 as! Failure }) 72 | .receive(subscriber: subscriber) 73 | } 74 | 75 | public func send(_ value: Output) { 76 | parent.base.send((id, .success(value))) 77 | } 78 | 79 | public func send(completion: Subscribers.Completion) { 80 | switch completion { 81 | case .finished: 82 | parent.base.send(completion: .finished) 83 | case .failure(let error): 84 | parent.base.send((id, .failure(error))) 85 | } 86 | } 87 | 88 | public func send(subscription: Subscription) { 89 | parent.base.send(subscription: subscription) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Process/Process.StandardOutputSink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import FoundationX 6 | import Swift 7 | import System 8 | 9 | public enum _ProcessStandardOutputSink: Hashable { 10 | /// Redirect output to the terminal. 11 | case terminal 12 | /// Redirect output to the file at the given path, creating if necessary. 13 | case filePath(_ path: String) 14 | /// Redirect output and error streams to the files at the given paths, creating if necessary. 15 | case split(_ out: String, err: String) 16 | /// The null device, also known as `/dev/null`. 17 | case null 18 | } 19 | 20 | #if os(macOS) 21 | extension Process { 22 | public typealias StandardOutputSink = _ProcessStandardOutputSink 23 | } 24 | 25 | // MARK: - Initializers 26 | 27 | extension Process.StandardOutputSink { 28 | public static func file( 29 | _ url: URL 30 | ) -> Self { 31 | Self.filePath(url._fromFileURLToURL().path) 32 | } 33 | } 34 | 35 | // MARK: - Supplementary 36 | 37 | extension Process { 38 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) 39 | public func redirectAllOutput( 40 | to sink: Process.StandardOutputSink 41 | ) throws { 42 | switch sink { 43 | case .terminal: 44 | self.standardOutput = FileHandle.standardOutput 45 | self.standardError = FileHandle.standardError 46 | case .filePath(let path): 47 | let fileHandle: FileHandle = try self._createFile(atPath: path) 48 | 49 | self.standardOutput = fileHandle 50 | self.standardError = fileHandle 51 | case .split(let outputPath, let errorPath): 52 | self.standardOutput = try self._createFile(atPath: outputPath) 53 | self.standardError = try self._createFile(atPath: errorPath) 54 | case .null: 55 | self.standardOutput = FileHandle.nullDevice 56 | self.standardError = FileHandle.nullDevice 57 | } 58 | } 59 | 60 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) 61 | private func _createFile( 62 | atPath path: String 63 | ) throws -> FileHandle { 64 | let directories = FilePath(path).lexicallyNormalized().removingLastComponent() 65 | 66 | try FileManager.default.createDirectory(atPath: directories.string, withIntermediateDirectories: true) 67 | 68 | guard FileManager.default.createFile(atPath: path, contents: Data()) else { 69 | struct CouldNotCreateFile: Error { 70 | let path: String 71 | } 72 | 73 | throw CouldNotCreateFile(path: path) 74 | } 75 | 76 | guard let fileHandle = FileHandle(forWritingAtPath: path) else { 77 | struct CouldNotOpenFileForWriting: Error { 78 | let path: String 79 | } 80 | 81 | throw CouldNotOpenFileForWriting(path: path) 82 | } 83 | 84 | return fileHandle 85 | } 86 | } 87 | 88 | #endif 89 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/WIP/_AsyncTaskScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Combine 6 | import Swift 7 | 8 | /// WIP, not super well thought out. 9 | public protocol _AsyncTaskScheduler { 10 | func schedule( 11 | _ task: @Sendable @escaping () async -> Void 12 | ) 13 | 14 | func _performCancellable( 15 | @_implicitSelfCapture operation: @Sendable @escaping () async -> T 16 | ) async -> Result 17 | 18 | func perform( 19 | @_implicitSelfCapture operation: @Sendable @escaping () async -> T 20 | ) async throws -> T 21 | } 22 | 23 | // MARK: - Implementation 24 | 25 | extension _AsyncTaskScheduler { 26 | public func _performCancellable( 27 | operation: @escaping @Sendable () async -> T 28 | ) async -> Result { 29 | await withUnsafeContinuation { continuation in 30 | schedule { 31 | do { 32 | try Task.checkCancellation() 33 | 34 | continuation.resume(returning: Result.success(await operation())) 35 | 36 | try Task.checkCancellation() 37 | } catch { 38 | continuation.resume(returning: Result.failure(CancellationError())) 39 | } 40 | } 41 | } 42 | } 43 | 44 | public func perform( 45 | @_implicitSelfCapture operation: @Sendable @escaping () async -> T 46 | ) async throws -> T { 47 | try await _performCancellable(operation: operation).get() 48 | } 49 | } 50 | 51 | // MARK: - Conformees 52 | 53 | public struct _DefaultAsyncScheduler { 54 | public func schedule( 55 | _ task: @Sendable @escaping () async -> Void 56 | ) { 57 | Task { 58 | await task() 59 | } 60 | } 61 | 62 | public func perform( 63 | @_implicitSelfCapture operation: @Sendable @escaping () async -> T 64 | ) async -> Result { 65 | do { 66 | let result = await operation() 67 | 68 | try Task.checkCancellation() 69 | 70 | return .success(result) 71 | } catch { 72 | return .failure(CancellationError()) 73 | } 74 | } 75 | } 76 | 77 | extension TaskQueue: _AsyncTaskScheduler { 78 | public func schedule( 79 | _ task: @Sendable @escaping () async -> Void 80 | ) { 81 | addTask { 82 | await task() 83 | } 84 | } 85 | } 86 | 87 | extension ThrowingTaskQueue: _AsyncTaskScheduler { 88 | public func schedule( 89 | _ task: @Sendable @escaping () async -> Void 90 | ) { 91 | addTask { 92 | await withTaskCancellationHandler { 93 | await task() 94 | } onCancel: { 95 | assertionFailure() 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/CommandLineToolSupport/Intramodular/AnyCommandLineTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Diagnostics 6 | import Foundation 7 | import Merge 8 | import Runtime 9 | 10 | @available(macOS 11.0, *) 11 | @available(iOS, unavailable) 12 | @available(macCatalyst, unavailable) 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | open class AnyCommandLineTool: Logging { 16 | public lazy var logger = PassthroughLogger(source: self) 17 | 18 | public var environmentVariables: [String: any CLT.EnvironmentVariableValue] = [:] 19 | public var currentDirectoryURL: URL? = nil 20 | 21 | public init() { 22 | 23 | } 24 | 25 | @discardableResult 26 | open func withUnsafeSystemShell( 27 | perform operation: (SystemShell) async throws -> R 28 | ) async throws -> R { 29 | let environmentVariables = _resolveEnvironmentVariables() 30 | 31 | let shell = SystemShell( 32 | environment: environmentVariables.mapValues({ String(describing: $0) }), 33 | currentDirectoryURL: currentDirectoryURL ?? URL(fileURLWithPath: FileManager.default.currentDirectoryPath), 34 | options: [._forwardStdoutStderr] 35 | ) 36 | 37 | let result: R = try await operation(shell) 38 | 39 | return result 40 | } 41 | 42 | /// Resolves the full list of environment variables by combining manually set environment variables with runtime-reflected variables that are defined via the `@EnvironmentVariable` property wrapper. 43 | private func _resolveEnvironmentVariables() -> [String: any CLT.EnvironmentVariableValue] { 44 | var result: [String: any CLT.EnvironmentVariableValue] = environmentVariables 45 | 46 | let mirror = InstanceMirror(self)! 47 | 48 | for child in mirror.children { 49 | guard let propertyWrapper = child.value as? (any _CommandLineToolEnvironmentVariableProtocol) else { 50 | continue 51 | } 52 | 53 | let environmentVariableName = propertyWrapper.name 54 | let environmentVariableValue: any CLT.EnvironmentVariableValue = propertyWrapper.wrappedValue 55 | 56 | if environmentVariables.contains(key: environmentVariableName) { 57 | fatalError("conflict for \(environmentVariableName)") 58 | } 59 | 60 | result[environmentVariableName] = environmentVariableValue 61 | } 62 | 63 | return result 64 | } 65 | } 66 | 67 | @available(macOS 11.0, *) 68 | @available(iOS, unavailable) 69 | @available(macCatalyst, unavailable) 70 | @available(tvOS, unavailable) 71 | @available(watchOS, unavailable) 72 | extension AnyCommandLineTool { 73 | public func withUnsafeSystemShell( 74 | sink: _ProcessStandardOutputSink, 75 | perform operation: (SystemShell) async throws -> R 76 | ) async throws -> R { 77 | try await withUnsafeSystemShell { shell in 78 | shell.options ??= [] 79 | shell.options?.removeAll(where: { 80 | $0._stdoutStderrSink != .null 81 | }) 82 | shell.options?.append(._forwardStdoutStderr(to: sink)) 83 | 84 | return try await operation(shell) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/Merge/Intramodular/Utilities/Concurrency/_AsyncGenerationBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vatsal Manot 3 | // 4 | 5 | import Swallow 6 | 7 | public final class _AsyncGenerationBox: @unchecked Sendable { 8 | public typealias FulfilledValue = Result 9 | 10 | private let lock = OSUnfairLock() 11 | 12 | private let _generationIterator: MonotonicallyIncreasingID 13 | 14 | private var _lastGeneration: AnyHashable? 15 | private var _currentGeneration: AnyHashable 16 | private var _promises: [AnyHashable: _AsyncPromise] 17 | 18 | public var lastValue: FulfilledValue? { 19 | lock.withCriticalScope { 20 | _lastGeneration.flatMap({ _promises[$0] })?.fulfilledResult 21 | } 22 | } 23 | 24 | public init() { 25 | self._generationIterator = MonotonicallyIncreasingID() 26 | self._lastGeneration = nil 27 | self._currentGeneration = self._generationIterator.next() 28 | self._promises = [:] 29 | } 30 | 31 | public func fulfill( 32 | with result: Result 33 | ) { 34 | lock.withCriticalScope { 35 | guard let promise = _promises[_currentGeneration] else { 36 | return 37 | } 38 | 39 | promise.fulfill(with: result) 40 | 41 | if let _lastGeneration { 42 | _promises.removeValue(forKey: _lastGeneration) 43 | } 44 | 45 | _incrementGeneration() 46 | } 47 | } 48 | 49 | private func _incrementGeneration() { 50 | _lastGeneration = _currentGeneration 51 | _currentGeneration = _generationIterator.next() 52 | } 53 | 54 | public func result() async -> Result { 55 | let promise = lock.withCriticalScope { 56 | _promises[_currentGeneration, defaultInPlace: .init()] 57 | } 58 | 59 | return await promise.result() 60 | } 61 | } 62 | 63 | // MARK: - Extensions - 64 | 65 | extension _AsyncGenerationBox { 66 | public func fulfill(with success: Success) { 67 | fulfill(with: .success(success)) 68 | } 69 | 70 | public func fulfill(with failure: Failure) { 71 | fulfill(with: .failure(failure)) 72 | } 73 | 74 | public func get() async throws -> Success { 75 | try await result().get() 76 | } 77 | 78 | public func get() async -> Success where Failure == Never { 79 | await self.result().get() 80 | } 81 | } 82 | 83 | // MARK: - Conformances 84 | 85 | extension _AsyncGenerationBox: Cancellable where Failure == Error { 86 | public func cancel() { 87 | _promises.values.forEach({ $0.cancel() }) 88 | } 89 | } 90 | 91 | // MARK: - Error Handling 92 | 93 | extension _AsyncGenerationBox { 94 | fileprivate enum _Error: Swift.Error { 95 | case promiseAlreadyFulfilled 96 | } 97 | } 98 | 99 | // MARK: - Auxiliary 100 | 101 | private class MonotonicallyIncreasingID { 102 | private var currentValue: Int64 = 0 103 | 104 | public func next() -> AnyHashable { 105 | currentValue += 1 106 | return currentValue 107 | } 108 | } 109 | --------------------------------------------------------------------------------