I had run into the Future deinit crash and was losing my mind until I found this thread, so thanks for having such a detailed back and forth.
The demo code above helped me to set up the crash scenario. Here's an alternate solution for a ReplayOnce
publisher that behaves the same as a future but without the crash, backing it with a simple CurrentValueSubject
.
public extension Publishers {
typealias ReplayPromise<Output, Failure: Error> = (Result<Output, Failure>) -> Void
final class ReplayOnce<Output, Failure: Error>: Publisher {
private let currentValue = CurrentValueSubject<Output?, Failure>(nil)
private let outputValue: AnyPublisher<Output, Failure>
private let dataLock = NSRecursiveLock(name: "\(ReplayOnce.self):data_lock")
public init(_ handler: @escaping ((@escaping ReplayPromise<Output, Failure>) -> Void)) {
let current = currentValue
let lock = dataLock
let promise: ReplayPromise<Output, Failure> = { result in
lock.lock()
defer { lock.unlock() }
switch result {
case .success(let value):
current.send(value)
current.send(completion: .finished)
case .failure(let error):
current.send(completion: .failure(error))
}
}
defer {
handler(promise)
}
self.outputValue = currentValue
.compactMap { $0 }
.eraseToAnyPublisher()
}
// MARK: Publisher
public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
dataLock.lock()
defer { dataLock.unlock() }
if let value = currentValue.value {
let just = Just(value).setFailureType(to: Failure.self)
just.receive(subscriber: subscriber)
} else {
outputValue.receive(subscriber: subscriber)
}
}
}
}
with this class you can instiate it the same as you would a Future...
func fetch() -> AnyPublisher<Data, SomeError> {
return Publisher.ReplayOnce { promise in
queue.async {
/// some work
if let data = data {
promise(.success(data)
} else {
promise(.failure(someError))
}
}
}
.eraseToAnyPublisher()
}