# Concurrency Control Mechanism in BitChat's BLE Media Transfers: How It Works

> Discover BitChat's concurrency control for BLE media transfers. Learn how NSLock, serial queues, and panic recovery ensure atomic file operations and prevent race conditions.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-08

---

**BitChat serializes all BLE media transfer operations through a centralized NSLock-based coordination layer in `BLEIncomingFileStore`, combining atomic file writes, serial dispatch queues, and durable panic-recovery markers to eliminate race conditions during concurrent file operations.**

BitChat, an open-source peer-to-peer messaging application by permissionlesstech, handles media transfers over Bluetooth Low Energy (BLE) with strict concurrency guarantees. The concurrency control mechanism in BitChat's BLE media transfers relies on a lightweight but robust locking strategy implemented in the `BLEIncomingFileStore` class. This deterministic design ensures that simultaneous inbound and outbound file operations never corrupt the payload store, even under heavy load or process interruption.

## Core Concurrency Components

BitChat's BLE media layer employs four complementary mechanisms to coordinate access to shared file system resources.

### PayloadCoordination and NSLock

The primary synchronization primitive is **`PayloadCoordination`**, a small coordinator class that encapsulates an **`NSLock`** and two critical mutable sets: `pendingDeliveryPaths` (files that have just been written) and `deletionReservations` (paths reserved for pending deletion). According to the source code in [[`BLEIncomingFileStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEIncomingFileStore.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEIncomingFileStore.swift#L110-L118), all mutations to these sets are wrapped in `payloadCoordination.lock.lock()` and `unlock()` calls. This guarantees that only one thread can add, remove, or query the delivery and deletion states at any given moment.

### Atomic File Writes

When persisting payload data, BitChat uses **`Data.WritingOptions.atomic`** (and on iOS, `.completeFileProtectionUntilFirstUserAuthentication`). As implemented in the [`save()`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEIncomingFileStore.swift#L23-L30) method, the atomic flag forces the system to write to a temporary file and then rename it to the final destination. This prevents other threads from observing partially-written files during concurrent access.

### Serial BLE Dispatch Queue

The BLE stack itself runs on a dedicated serial **`DispatchQueue`** named `messageQueue`, identified by a queue-specific key. As shown in [[`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift#L376-L383), all BLE message handling—including the hand-off of media payloads—asserts execution on this queue. This ensures that no two Bluetooth events race each other before reaching the file store.

### File-System Markers for Crash Recovery

BitChat implements a two-phase commit mechanism using panic-recovery markers (`.panic-recovery-pending`). Before any media wipe operation, the [`markPanicRecoveryPending()`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEIncomingFileStore.swift#L58-L66) method writes a durable marker that survives process crashes. The marker is deleted only after the wipe succeeds, preventing simultaneous recovery operations from corrupting application state.

## Implementation in Code

The following examples demonstrate how these mechanisms work together in the permissionlesstech/bitchat codebase.

### Serializing Media Writes with NSLock

Every public entry point in `BLEIncomingFileStore` grabs the coordination lock before touching mutable state or the file system. The `save()` method exemplifies this pattern:

```swift
func save(
    data: Data,
    preferredName: String?,
    subdirectory: String,
    fallbackExtension: String?,
    defaultPrefix: String
) -> URL? {
    payloadCoordination.lock.lock()          // ← exclusive access begins
    defer { payloadCoordination.lock.unlock() }

    do {
        let base = try filesDirectory().appendingPathComponent(subdirectory, isDirectory: true)
        try fileManager.createDirectory(at: base, withIntermediateDirectories: true,
                                         attributes: Self.mediaProtectionAttributes)

        let sanitized = sanitizedFileName(preferredName,
                                          defaultName: "\(defaultPrefix)_\(Self.timestampString(from: dateProvider()))",
                                          fallbackExtension: fallbackExtension)

        let destination = uniqueFileURL(in: base,
                                        fileName: sanitized,
                                        reservedPaths: /* … */,
                                        forceRandomizedName: false)

        var options: Data.WritingOptions = [.atomic]          // atomic write
        #if os(iOS)
        options.insert(.completeFileProtectionUntilFirstUserAuthentication)
        #endif
        try data.write(to: destination, options: options)    // ← atomic commit
        payloadCoordination.pendingDeliveryPaths.insert(destination.standardizedFileURL.path)
        return destination
    } catch {
        SecureLogger.error("❌ Failed to persist incoming media: \(error)", category: .session)
        return nil
    }
}

```

The lock guarantees that `pendingDeliveryPaths` cannot be mutated concurrently by another thread, while the atomic write ensures the file appears on disk only after the entire payload is committed.

### Coordinating Deletion Reservations

When reserving media for deletion, the `reservePrivateMediaDeletion()` function checks for conflicts under the same lock:

```swift
func reservePrivateMediaDeletion(messageIDs: [String],
                                 payloadRelativePaths: [String: String])
    -> PrivateMediaDeletionReservation? {

    payloadCoordination.lock.lock()
    defer { payloadCoordination.lock.unlock() }

    guard let paths = privateMediaReceipts
        .prospectiveDeletionPayloadPaths(messageIDs: messageIDs,
                                         payloadRelativePaths: payloadRelativePaths),
          paths.isDisjoint(with: payloadCoordination.pendingDeliveryPaths) else {
        return nil          // another thread already owns these paths
    }

    let reservation = PrivateMediaDeletionReservation(id: UUID())
    payloadCoordination.deletionReservations[reservation.id] = paths
    return reservation
}

```

Only one thread can create a reservation for a specific set of paths; concurrent attempts see the conflict and return `nil`, preventing double-deletion or deletion of actively transferring files.

### Enforcing Queue Serialization

Within [[`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift), the serial queue ensures ordered processing of all Bluetooth events:

```swift
// Inside BLEService.swift
private let messageQueue = DispatchQueue(label: "mesh.message")
private let messageQueueKey = DispatchSpecificKey<Void>()
messageQueue.setSpecific(key: messageQueueKey, value: ())

func handleIncoming(_ data: Data) {
    // Ensure we are on the serial BLE queue
    assert(DispatchQueue.getSpecific(key: messageQueueKey) != nil,
           "BLE handling must run on messageQueue")
    // …process data, possibly calling BLEIncomingFileStore.save(...)
}

```

All BLE-related work is forced onto `messageQueue`, so even if multiple Bluetooth events arrive simultaneously, they are processed sequentially before invoking the file store.

## Summary

- **Centralized Locking**: A single `NSLock` inside `PayloadCoordination` protects all mutable state in `BLEIncomingFileStore`, ensuring thread-safe access to `pendingDeliveryPaths` and `deletionReservations`.
- **Atomic Persistence**: All file writes use `Data.WritingOptions.atomic`, which writes to temporary storage and renames on completion, preventing corruption from partial writes.
- **Serial Queue**: The dedicated `messageQueue` in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) serializes all BLE event handling before they reach the media store.
- **Crash Safety**: Panic-recovery markers provide a durable two-phase commit mechanism that survives process crashes, ensuring media wipes are atomic and recoverable.

## Frequently Asked Questions

### What type of lock does BitChat use for BLE media concurrency control?

BitChat uses an **`NSLock`** encapsulated within the `PayloadCoordination` class located in [`BLEIncomingFileStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEIncomingFileStore.swift). This is a traditional mutex lock that blocks concurrent threads from entering critical sections that manipulate the `pendingDeliveryPaths` and `deletionReservations` sets or perform file system operations.

### How does BitChat prevent other threads from reading incomplete media files?

BitChat employs **atomic file writes** using `Data.WritingOptions.atomic` (and `.completeFileProtectionUntilFirstUserAuthentication` on iOS). This option forces the operating system to write data to a temporary file and perform an atomic rename operation only after the write completes successfully. Consequently, other threads cannot observe partially-written files because the destination path does not exist until the entire payload is committed to disk.

### Why does BitChat use a serial dispatch queue for BLE operations?

The serial **`DispatchQueue`** named `messageQueue` (defined in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift)) ensures that all Bluetooth-related callbacks and message processing happen sequentially rather than concurrently. This eliminates race conditions at the transport layer and guarantees that media transfer hand-offs to `BLEIncomingFileStore` occur in a predictable order, simplifying the concurrency model above the file system layer.

### What happens if BitChat crashes during a media deletion operation?

BitChat implements **panic-recovery markers** (`.panic-recovery-pending` files) that act as durable locks surviving process crashes. Before wiping media, the `markPanicRecoveryPending()` method creates a marker; after successful deletion, the marker is removed. If a crash occurs, the presence of the marker on restart triggers recovery logic that prevents simultaneous or duplicate wipe operations from corrupting the media store state.