mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +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:
41
apple/VniDrop/Core/CoreDispatcher.swift
Normal file
41
apple/VniDrop/Core/CoreDispatcher.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
|
||||
/// Dispatch-queue labels for the core's serial and interrupt lanes.
|
||||
enum QueueLabel {
|
||||
static let core = "com.vnidrop.core"
|
||||
static let interrupt = "com.vnidrop.core.interrupt"
|
||||
}
|
||||
|
||||
/// Two-lane dispatcher for blocking core calls.
|
||||
///
|
||||
/// `run` serializes calls on one queue so the core is driven from a single lane.
|
||||
/// `runInterrupt` uses a *separate* concurrent lane, so an interrupt-style call
|
||||
/// (cancel) can reach the core while a blocking call (`receive`) still occupies
|
||||
/// the serial lane. The core is internally synchronized and explicitly supports
|
||||
/// cancel arriving from another thread mid-receive (see VnidropCore.block_on);
|
||||
/// a single shared queue would deadlock it.
|
||||
final class CoreDispatcher: Sendable {
|
||||
private let serialQueue: DispatchQueue
|
||||
private let interruptQueue: DispatchQueue
|
||||
|
||||
init(label: String = QueueLabel.core, interruptLabel: String = QueueLabel.interrupt) {
|
||||
serialQueue = DispatchQueue(label: label, qos: .userInitiated)
|
||||
interruptQueue = DispatchQueue(label: interruptLabel, qos: .userInitiated, attributes: .concurrent)
|
||||
}
|
||||
|
||||
/// Runs a blocking core call on the serial lane and hops the result back.
|
||||
func run<T: Sendable>(_ block: @escaping @Sendable () throws -> T) async -> Result<T, Error> {
|
||||
await withCheckedContinuation { continuation in
|
||||
serialQueue.async { continuation.resume(returning: Result { try block() }) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `run`, 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.
|
||||
func runInterrupt<T: Sendable>(_ block: @escaping @Sendable () throws -> T) async -> Result<T, Error> {
|
||||
await withCheckedContinuation { continuation in
|
||||
interruptQueue.async { continuation.resume(returning: Result { try block() }) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user