# Managing Project Save and Close with Concurrent Mutations in Palmier Pro

> Learn how Palmier Pro manages project save and close with concurrent mutations. Discover how ProjectPackageCoordinator serializes access and ensures data integrity during operations.

- Repository: [Palmier/palmier-pro](https://github.com/palmier-io/palmier-pro)
- Tags: internals
- Published: 2026-07-27

---

**Palmier Pro uses the `ProjectPackageCoordinator` to serialize access to the .palmier package, ensuring that mutations pause during saves and close operations wait until all background work completes.**

Palmier Pro stores a project's media assets and timeline in a **.palmier** package. Because the UI runs on the main actor while file-system work and media processing happen off the main thread, the app requires a robust coordination layer to prevent data races. The `ProjectPackageCoordinator` class, defined in [`Sources/PalmierPro/Project/ProjectPackageCoordinator.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Project/ProjectPackageCoordinator.swift), guarantees that no mutation runs while a save or close is in progress.

## The Concurrency Challenge in Palmier Pro

Concurrent mutations pose a significant risk when users edit timelines while auto-save operations run in the background. If a mutation modifies the timeline while the save operation is serializing the project to disk, the resulting **.palmier** package could contain inconsistent state. Additionally, closing a project while mutations are active can lead to resource leaks or corrupted persistence.

The coordinator solves this by tracking three pieces of state:

- **savesInProgress**: An integer counting active save operations.
- **activeMutations**: An integer counting currently running mutations.
- **pendingMutations**: A queue holding mutations that arrived while a save was ongoing.

## Inside the ProjectPackageCoordinator

`ProjectPackageCoordinator` is annotated with **@MainActor**, ensuring all state changes happen on the UI thread while the heavy work inside mutations runs on background executors. This design prevents race conditions when the view-model in [`Sources/PalmierPro/Editor/ViewModel/EditorViewModel.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Editor/ViewModel/EditorViewModel.swift) forwards user actions to the coordinator.

### Tracking Save and Mutation State

When a save begins, the coordinator calls `saveStarted()` to increment **savesInProgress**. Each mutation wraps its execution in `beginMutation()` and `endMutation()` calls that update **activeMutations**. If `saveFinished(success:)` reports failure, the coordinator cancels all pending mutations rather than applying them to a corrupted project.

### Queuing Mutations During Active Saves

The `performMutation(_:)` method checks **savesInProgress** before executing work. If a save is active, the mutation stores its continuation in **pendingMutations** and suspends. Once `saveFinished(success:)` completes successfully, `resumeIdleWaitersIfNeeded()` resumes these queued mutations.

## Implementing Safe Mutations

Editor commands use the coordinator to bracket mutation work. The following pattern from [`EditorViewModel.swift`](https://github.com/palmier-io/palmier-pro/blob/main/EditorViewModel.swift) demonstrates how to add a clip while respecting save boundaries:

```swift
func addClip(_ clip: Clip) async throws {
    try projectPackageCoordinator.beginMutation()
    defer { projectPackageCoordinator.endMutation() }

    let result = try await projectPackageCoordinator.performMutation {
        timeline.add(clip)
        return true
    }

    if result { onProjectCheckpointRequired?() }
}

```

The `beginMutation()` call blocks if a save is in progress, while `defer` ensures `endMutation()` runs even if the mutation throws. The closure passed to `performMutation` executes on a background queue, keeping the main actor responsive.

## Graceful Project Shutdown

Closing a project requires a two-phase handshake. First, `beginClosing()` sets **isClosing** to true, preventing new mutations from starting. Then `waitUntilIdle()` suspends the caller until both **savesInProgress** and **activeMutations** reach zero.

```swift
func closeProject() async {
    await projectPackageCoordinator.beginClosing()
    await projectPackageCoordinator.waitUntilIdle()

    videoEngine?.shutdown()
    projectPackageCoordinator.cancelClosing()
}

```

The `waitUntilIdle()` method uses a `CheckedContinuation` stored in **idleWaiters**. When the project becomes idle, `resumeIdleWaitersIfNeeded()` resumes all waiters, allowing resource teardown to proceed safely.

## Error Handling and Cancellation Safety

The coordinator defends against unbalanced state updates. Mismatched calls to `saveFinished` or `endMutation` trigger `assertionFailure`, catching programming errors during development. For cancellation, `performMutation(_:)` wraps its continuation with `withTaskCancellationHandler`. If the caller cancels the task, `cancelMutation(id:)` removes the pending mutation from the queue and invokes its cancellation closure.

Failed saves also trigger cleanup. When `saveFinished(success: false)` executes, the coordinator discards all **pendingMutations** rather than applying them to the project state:

```swift
func saveProject() async {
    projectPackageCoordinator.saveStarted()
    defer { projectPackageCoordinator.saveFinished(success: true) }

    do {
        try await fileIO.writeProject(to: projectURL)
    } catch {
        projectPackageCoordinator.saveFinished(success: false)
        throw error
    }
}

```

## Summary

- **`ProjectPackageCoordinator`** in [`Sources/PalmierPro/Project/ProjectPackageCoordinator.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Project/ProjectPackageCoordinator.swift) centralizes concurrency control for the **.palmier** package format.
- **@MainActor** isolation ensures thread-safe state updates while mutations run on background queues.
- **Three state trackers**—`savesInProgress`, `activeMutations`, and `pendingMutations`—coordinate active work and queue deferred operations.
- **Closing sequence** uses `beginClosing()` and `waitUntilIdle()` to guarantee resources are released only after all saves and mutations complete.
- **Cancellation handlers** prevent memory leaks when async mutations are cancelled mid-flight.

## Frequently Asked Questions

### How does Palmier Pro prevent mutations from corrupting a save operation?

`ProjectPackageCoordinator` increments `savesInProgress` when `saveStarted()` is called. If `performMutation(_:)` detects a non-zero count, it stores the mutation in `pendingMutations` instead of executing it immediately. After `saveFinished(success:)` completes, pending mutations resume, ensuring no overlap between save I/O and state changes.

### What happens to pending mutations if a project save fails?

When `saveFinished(success: false)` is invoked, the coordinator cancels all mutations held in `pendingMutations` by invoking their cancellation handlers. This prevents partial edits from being applied to a project that failed to persist, maintaining state consistency.

### Why is ProjectPackageCoordinator marked with @MainActor?

The **@MainActor** annotation ensures that counter increments, continuation storage, and idle-waiter management happen on the main thread. While the coordinator schedules actual mutation work on background executors, the synchronization primitives themselves remain thread-safe by virtue of Swift's actor isolation, preventing data races in the concurrency state machine.

### How does the close operation know when the project is truly idle?

The `waitUntilIdle()` method creates a `CheckedContinuation` and appends it to `idleWaiters`. Whenever `endMutation()` or `saveFinished()` reduces `activeMutations` or `savesInProgress` to zero, `resumeIdleWaitersIfNeeded()` resumes all stored continuations. This suspends the close routine until both counters reach zero, guaranteeing no background work is active during resource teardown.