summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/Operations/ExclusivityController.swift
blob: d198ffd49707a74bd37632076bc50b2e8b6b258d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//
//  ExclusivityController.swift
//  MullvadVPN
//
//  Created by pronebird on 06/07/2020.
//  Copyright © 2020 Mullvad VPN AB. All rights reserved.
//

import Foundation

class ExclusivityController: NSObject {
    private let lock = NSLock()
    private var operations: [String: [Operation]] = [:]
    private var categoriesByOperation: [Operation: [String]] = [:]

    static let shared = ExclusivityController()

    func addOperation(_ operation: Operation, categories: [String]) {
        lock.withCriticalBlock {
            categories.forEach { category in
                addOperation(operation, category: category)
            }

            addObserverIfNeeded(operation: operation, categories: categories)
        }
    }

    func removeOperation(_ operation: Operation, categories: [String]) {
        lock.withCriticalBlock {
            categories.forEach { category in
                removeOperation(operation, category: category)
            }

            removeObserverIfNeeded(operation: operation, categories: categories)
        }
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if let operation = object as? Operation, keyPath == "isFinished" {
            operationDidFinish(operation)
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }

    // MARK: - Private

    private func addOperation(_ operation: Operation, category: String) {
        var operationsWithThisCategory = operations[category] ?? []

        if let last = operationsWithThisCategory.last {
            operation.addDependency(last)
        }

        operationsWithThisCategory.append(operation)

        operations[category] = operationsWithThisCategory
    }

    private func removeOperation(_ operation: Operation, category: String) {
        guard var operationsWithThisCategory = operations[category],
              let index = operationsWithThisCategory.firstIndex(of: operation) else { return }

        operationsWithThisCategory.remove(at: index)

        if operationsWithThisCategory.isEmpty {
            operations.removeValue(forKey: category)
        } else {
            operations[category] = operationsWithThisCategory
        }
    }

    private func addObserverIfNeeded(operation: Operation, categories: [String]) {
        let existingCategories = categoriesByOperation[operation] ?? []
        let newCategories = existingCategories + categories

        if existingCategories.isEmpty && !newCategories.isEmpty {
            operation.addObserver(self, forKeyPath: "isFinished", options: .new, context: nil)
        }

        if !newCategories.isEmpty {
            categoriesByOperation[operation] = newCategories
        }
    }

    private func removeObserverIfNeeded(operation: Operation, categories: [String]) {
        guard var newCategories = categoriesByOperation[operation] else { return }

        newCategories.removeAll { s in
            categories.contains(s)
        }

        if newCategories.isEmpty {
            operation.removeObserver(self, forKeyPath: "isFinished", context: nil)

            categoriesByOperation.removeValue(forKey: operation)
        } else {
            categoriesByOperation[operation] = newCategories
        }
    }

    private func operationDidFinish(_ operation: Operation) {
        lock.withCriticalBlock {
            let operationCategories = categoriesByOperation[operation] ?? []

            removeObserverIfNeeded(operation: operation, categories: operationCategories)

            operationCategories.forEach { category in
                removeOperation(operation, category: category)
            }
        }
    }
}