mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
fix(apple): make receive-cancel actually cancel the transfer
The Cancel button on an in-progress receive did nothing. CoreRepository funnelled every core call through one serial DispatchQueue, but `receive` is a blocking core call that occupies that queue for the whole transfer. The tapped `cancelTransfer` was enqueued behind the in-flight `receive` on the same serial queue, so it could never run until `receive` returned — which it never would, because it was waiting to be cancelled. A deadlock the button couldn't escape. The Rust core is explicitly designed for cancel to arrive from another thread mid-receive (VnidropCore.block_on uses a shared runtime handle for exactly this). Extracts the two-lane dispatch into a CoreDispatcher: a serial lane for ordered calls and a separate concurrent lane for interrupt-style calls, and routes cancel through the latter so the signal reaches the core and unblocks the receive. Adds CoreDispatcherTests, including a regression guard that an interrupt completes while the serial lane is blocked.
This commit is contained in:
@@ -21,7 +21,7 @@ final class CoreRepository: ObservableObject, CoreGateway {
|
||||
// `runCore`; the underlying core is internally synchronized, so this crossing
|
||||
// is safe. `nonisolated(unsafe)` documents that contract for Swift 6.
|
||||
private nonisolated(unsafe) var core: VnidropCore?
|
||||
private let queue = DispatchQueue(label: "com.vnidrop.core", qos: .userInitiated)
|
||||
private let dispatcher = CoreDispatcher()
|
||||
private lazy var sink = RepositoryEventSink { [weak self] event in
|
||||
Task { @MainActor in self?.handle(event: event) }
|
||||
}
|
||||
@@ -119,7 +119,9 @@ final class CoreRepository: ObservableObject, CoreGateway {
|
||||
// MARK: - Lifecycle actions
|
||||
|
||||
func cancel(transferId: UInt64) async -> Result<Void, Error> {
|
||||
await runCore {
|
||||
// Off the serial `queue`: a receive in flight is blocking it, and the
|
||||
// cancel signal must reach the core to unblock that receive.
|
||||
await runInterrupt {
|
||||
try self.requireCore().cancelTransfer(transferId: transferId)
|
||||
}.map { self.refreshSnapshot() }
|
||||
}
|
||||
@@ -253,17 +255,14 @@ final class CoreRepository: ObservableObject, CoreGateway {
|
||||
|
||||
/// Runs a blocking core call off the main actor and hops the result back.
|
||||
private nonisolated func runCore<T: Sendable>(_ block: @escaping @Sendable () throws -> T) async -> Result<T, Error> {
|
||||
await withCheckedContinuation { continuation in
|
||||
queue.async {
|
||||
let result: Result<T, Error>
|
||||
do {
|
||||
result = .success(try block())
|
||||
} catch {
|
||||
result = .failure(error)
|
||||
}
|
||||
continuation.resume(returning: result)
|
||||
}
|
||||
}
|
||||
await dispatcher.run(block)
|
||||
}
|
||||
|
||||
/// Like `runCore`, but off the serial lane so it can interrupt a blocking
|
||||
/// call in flight there (e.g. cancel a `receive`). Only use for core calls
|
||||
/// that are safe to run concurrently with another core call.
|
||||
private nonisolated func runInterrupt<T: Sendable>(_ block: @escaping @Sendable () throws -> T) async -> Result<T, Error> {
|
||||
await dispatcher.runInterrupt(block)
|
||||
}
|
||||
|
||||
private nonisolated static func nextTransferId() -> UInt64 {
|
||||
|
||||
Reference in New Issue
Block a user