summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-09-15 11:01:42 +0200
committerAndrej Mihajlov <and@mullvad.net>2021-09-15 11:01:42 +0200
commitd336fcb0f55336373e076745fc3394232d7ca44a (patch)
tree3ce3aec7a58c069b55a12e0d8b3b4dfbea5a3636
parent0637057254b2a9b24d29ce87df3c6f49d16b4cf6 (diff)
parenta63cba4a2d2eb237207fd77c4120dc13928df47e (diff)
downloadmullvadvpn-d336fcb0f55336373e076745fc3394232d7ca44a.tar.xz
mullvadvpn-d336fcb0f55336373e076745fc3394232d7ca44a.zip
Merge branch 'promise-extensions'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj16
-rw-r--r--ios/MullvadVPN/Operations/ExclusivityController.swift118
-rw-r--r--ios/MullvadVPN/Promise/Promise+BackgroundTask.swift57
-rw-r--r--ios/MullvadVPN/Promise/Promise+OperationQueue.swift49
-rw-r--r--ios/MullvadVPN/Promise/Promise+Optional.swift7
-rw-r--r--ios/MullvadVPN/Promise/Promise+ReceiveOn.swift34
-rw-r--r--ios/MullvadVPN/Promise/Promise+Result.swift41
-rw-r--r--ios/MullvadVPN/Promise/Promise.swift39
-rw-r--r--ios/MullvadVPN/Promise/PromiseCompletion.swift17
-rw-r--r--ios/MullvadVPNTests/PromiseTests.swift51
10 files changed, 363 insertions, 66 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 6158d44473..15263a1696 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -26,6 +26,7 @@
581503A724D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; };
581CBCEE229826FD00727D7F /* StaticTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */; };
581FC4FA2695ACE100AA97BA /* Account.strings in Resources */ = {isa = PBXBuildFile; fileRef = 581FC4F82695ACE100AA97BA /* Account.strings */; };
+ 5820674926E63EC900655B05 /* Promise+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */; };
5823FA5026CA690600283BF8 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; };
58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */; };
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; };
@@ -51,6 +52,8 @@
584592612639B4A200EF967F /* ConsentContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* ConsentContentView.swift */; };
5845F842236CBACD00B2D93C /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; };
5845F843236CBDAB00B2D93C /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; };
+ 5846226726E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */; };
+ 5846226826E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */; };
584789B8264D4A2A000E45FB /* old_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B4264D4A2A000E45FB /* old_le_root_cert.cer */; };
584789B9264D4A2A000E45FB /* old_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B4264D4A2A000E45FB /* old_le_root_cert.cer */; };
584789BE264D4A2A000E45FB /* new_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B7264D4A2A000E45FB /* new_le_root_cert.cer */; };
@@ -132,6 +135,7 @@
589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589AB4F6227B64450039131E /* BasicTableViewCell.swift */; };
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; };
58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; };
+ 58A94AE626D23C3D001CB97C /* PromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE526D23C3D001CB97C /* PromiseTests.swift */; };
58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* ConsentViewController.swift */; };
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */; };
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */; };
@@ -160,6 +164,7 @@
58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; };
58BA791B2578F092006FAEA0 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58BA791A2578F092006FAEA0 /* WireGuardKit */; };
58BA7947257901A5006FAEA0 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58BA7946257901A5006FAEA0 /* WireGuardKit */; };
+ 58BF345E26F09F3C002A6CAA /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; };
58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; };
58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; };
58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
@@ -312,6 +317,7 @@
581503A524D6F4AE00C9C50E /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTableViewDataSource.swift; sourceTree = "<group>"; };
581FC4F92695ACE100AA97BA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Account.strings; sourceTree = "<group>"; };
+ 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+BackgroundTask.swift"; sourceTree = "<group>"; };
5823FA4F26CA690600283BF8 /* OSLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = "<group>"; };
58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewController.swift; sourceTree = "<group>"; };
58293FB025124117005D0BB5 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
@@ -332,6 +338,7 @@
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; };
584592602639B4A200EF967F /* ConsentContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentContentView.swift; sourceTree = "<group>"; };
5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelIpc.swift; sourceTree = "<group>"; };
+ 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+OperationQueue.swift"; sourceTree = "<group>"; };
584789B4264D4A2A000E45FB /* old_le_root_cert.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = old_le_root_cert.cer; sourceTree = "<group>"; };
584789B7264D4A2A000E45FB /* new_le_root_cert.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = new_le_root_cert.cer; sourceTree = "<group>"; };
584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLPinningURLSessionDelegate.swift; sourceTree = "<group>"; };
@@ -771,6 +778,8 @@
58E1337426D2BEC400CC316B /* Promise+Optional.swift */,
58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */,
58E1338026D2BF5C00CC316B /* Promise+Result.swift */,
+ 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */,
+ 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */,
);
path = Promise;
sourceTree = "<group>";
@@ -1062,14 +1071,17 @@
584E96BE240FD4DB00D3334F /* Location.swift in Sources */,
5857F23F24C844AD00CF6F47 /* Locking.swift in Sources */,
5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */,
+ 58BF345E26F09F3C002A6CAA /* ExclusivityController.swift in Sources */,
58E1338326D2BF5C00CC316B /* Promise+Result.swift in Sources */,
58B0A2AC238EE6D500BC001D /* IPAddress+Codable.swift in Sources */,
58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */,
+ 5846226826E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */,
5860392B26DCEE6300554C79 /* PromiseCompletion.swift in Sources */,
58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */,
5860392726D91B8400554C79 /* PromiseTests.swift in Sources */,
58E1337326D2BE9C00CC316B /* AnyOptional.swift in Sources */,
5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */,
+ 58A94AE626D23C3D001CB97C /* PromiseTests.swift in Sources */,
58C3478B26C1094F0060838B /* Promise.swift in Sources */,
5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */,
582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */,
@@ -1152,6 +1164,7 @@
584E96BC240FD4DA00D3334F /* Location.swift in Sources */,
581503A124D6F01F00C9C50E /* LogRotation.swift in Sources */,
58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */,
+ 5846226726E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */,
5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */,
58B67B482602079E008EF58E /* RelaySelector.swift in Sources */,
58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */,
@@ -1160,6 +1173,7 @@
5873884D239E6D7E00E96C4E /* EmbeddedViewContainerView.swift in Sources */,
583BC70724FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */,
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */,
+ 5820674926E63EC900655B05 /* Promise+BackgroundTask.swift in Sources */,
58B9EB132488ED2100095626 /* AlertPresenter.swift in Sources */,
587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */,
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */,
@@ -1233,6 +1247,8 @@
58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */,
58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */,
58B93A2526C683B300A55733 /* Promise.swift in Sources */,
+ 58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */,
+ 58B93A2526C683B300A55733 /* Promise.swift in Sources */,
587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */,
58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */,
5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */,
diff --git a/ios/MullvadVPN/Operations/ExclusivityController.swift b/ios/MullvadVPN/Operations/ExclusivityController.swift
index 9e18516869..b87fd93028 100644
--- a/ios/MullvadVPN/Operations/ExclusivityController.swift
+++ b/ios/MullvadVPN/Operations/ExclusivityController.swift
@@ -8,62 +8,108 @@
import Foundation
-class ExclusivityController<Category> where Category: Hashable {
- private let operationQueue: OperationQueue
- private let lock = NSRecursiveLock()
+class ExclusivityController: NSObject {
+ private let lock = NSLock()
+ private var operations: [String: [Operation]] = [:]
+ private var categoriesByOperation: [Operation: [String]] = [:]
- private var operations: [Category: [Operation]] = [:]
- private var observers: [Operation: NSObjectProtocol] = [:]
+ static let shared = ExclusivityController()
- init(operationQueue: OperationQueue) {
- self.operationQueue = operationQueue
- }
+ private override init() {}
+
+ func addOperation(_ operation: Operation, categories: [String]) {
+ lock.withCriticalBlock {
+ categories.forEach { category in
+ addOperation(operation, category: category)
+ }
- func addOperation(_ operation: Operation, categories: [Category]) {
- addOperations([operation], categories: categories)
+ addObserverIfNeeded(operation: operation, categories: categories)
+ }
}
- func addOperations(_ operations: [Operation], categories: [Category]) {
+ func removeOperation(_ operation: Operation, categories: [String]) {
lock.withCriticalBlock {
- for operation in operations {
- for category in categories {
- addDependencies(operation: operation, category: category)
- }
-
- observers[operation] = operation.observe(\.isFinished, options: [.initial, .new]) { [weak self] (op, change) in
- if let isFinished = change.newValue, isFinished {
- self?.operationDidFinish(op, categories: categories)
- }
- }
+ categories.forEach { category in
+ removeOperation(operation, category: category)
}
- operationQueue.addOperations(operations, waitUntilFinished: false)
+ removeObserverIfNeeded(operation: operation, categories: categories)
}
}
- private func addDependencies(operation: Operation, category: Category) {
- var exclusiveOperations = self.operations[category] ?? []
+ 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)
+ }
+ }
- if let dependency = exclusiveOperations.last, !operation.dependencies.contains(dependency) {
- operation.addDependency(dependency)
+ // MARK: - Private
+
+ private func addOperation(_ operation: Operation, category: String) {
+ var operationsWithThisCategory = operations[category] ?? []
+
+ if let last = operationsWithThisCategory.last {
+ operation.addDependency(last)
}
- exclusiveOperations.append(operation)
- self.operations[category] = exclusiveOperations
+ 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, categories: [Category]) {
+ private func operationDidFinish(_ operation: Operation) {
lock.withCriticalBlock {
- for category in categories {
- var exclusiveOperations = self.operations[category] ?? []
+ let operationCategories = categoriesByOperation[operation] ?? []
- exclusiveOperations.removeAll { (storedOperation) -> Bool in
- return operation == storedOperation
- }
+ removeObserverIfNeeded(operation: operation, categories: operationCategories)
- self.operations[category] = exclusiveOperations
+ operationCategories.forEach { category in
+ removeOperation(operation, category: category)
}
- self.observers.removeValue(forKey: operation)
}
}
}
diff --git a/ios/MullvadVPN/Promise/Promise+BackgroundTask.swift b/ios/MullvadVPN/Promise/Promise+BackgroundTask.swift
new file mode 100644
index 0000000000..1d7bfc3280
--- /dev/null
+++ b/ios/MullvadVPN/Promise/Promise+BackgroundTask.swift
@@ -0,0 +1,57 @@
+//
+// Promise+BackgroundTask.swift
+// Promise+BackgroundTask
+//
+// Created by pronebird on 06/09/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+extension Promise {
+
+ /// Start the background task for the duration of the upstream execution.
+ func requestBackgroundTime(taskName: String? = nil) -> Promise<Value> {
+ return Promise<Value> { resolver in
+ var backgroundTaskIdentifier: UIBackgroundTaskIdentifier?
+
+ let beginBackgroundTask = {
+ backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: taskName) {
+ resolver.resolve(completion: .cancelled)
+ }
+ }
+
+ let endBackgroundTask = {
+ guard let taskIdentifier = backgroundTaskIdentifier,
+ taskIdentifier != .invalid else { return }
+
+ UIApplication.shared.endBackgroundTask(taskIdentifier)
+ backgroundTaskIdentifier = nil
+ }
+
+ let endBackgroundTaskOnMainQueue = {
+ if Thread.isMainThread {
+ endBackgroundTask()
+ } else {
+ DispatchQueue.main.async(execute: endBackgroundTask)
+ }
+ }
+
+ if Thread.isMainThread {
+ beginBackgroundTask()
+ } else {
+ DispatchQueue.main.async(execute: beginBackgroundTask)
+ }
+
+ resolver.setCancelHandler {
+ endBackgroundTaskOnMainQueue()
+ }
+
+ self.observe { completion in
+ resolver.resolve(completion: completion)
+
+ endBackgroundTaskOnMainQueue()
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Promise/Promise+OperationQueue.swift b/ios/MullvadVPN/Promise/Promise+OperationQueue.swift
new file mode 100644
index 0000000000..be8c4e5166
--- /dev/null
+++ b/ios/MullvadVPN/Promise/Promise+OperationQueue.swift
@@ -0,0 +1,49 @@
+//
+// Promise+OperationQueue.swift
+// Promise+OperationQueue
+//
+// Created by pronebird on 02/09/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension Promise {
+
+ /// Returns a promise that adds operation that finishes along with the upstream.
+ func run(on operationQueue: OperationQueue) -> Promise<Value> {
+ return Promise { resolver in
+ let operation = AsyncBlockOperation { finish in
+ self.observe { completion in
+ resolver.resolve(completion: completion)
+ finish()
+ }
+ }
+
+ resolver.setCancelHandler {
+ operation.cancel()
+ }
+
+ operationQueue.addOperation(operation)
+ }
+ }
+
+ /// Returns a promise that adds a mutually exclusive operation that finishes along with the upstream.
+ func run(on operationQueue: OperationQueue, categories: [String]) -> Promise<Value> {
+ return Promise { resolver in
+ let operation = AsyncBlockOperation { finish in
+ self.observe { completion in
+ resolver.resolve(completion: completion)
+ finish()
+ }
+ }
+
+ resolver.setCancelHandler {
+ operation.cancel()
+ }
+
+ ExclusivityController.shared.addOperation(operation, categories: categories)
+ operationQueue.addOperation(operation)
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Promise/Promise+Optional.swift b/ios/MullvadVPN/Promise/Promise+Optional.swift
index 0fc38a1ae6..eca39ae3e6 100644
--- a/ios/MullvadVPN/Promise/Promise+Optional.swift
+++ b/ios/MullvadVPN/Promise/Promise+Optional.swift
@@ -28,4 +28,11 @@ extension Promise where Value: AnyOptional {
return value.asConcreteType().map(producePromise) ?? .resolved(defaultValue)
}
}
+
+ /// Map contained value to result providing failure when the value is `nil`.
+ func some<Failure: Error>(or failure: Failure) -> Result<Value.Wrapped, Failure>.Promise {
+ return then { value -> Result<Value.Wrapped, Failure> in
+ return value.asConcreteType().map { .success($0) } ?? .failure(failure)
+ }
+ }
}
diff --git a/ios/MullvadVPN/Promise/Promise+ReceiveOn.swift b/ios/MullvadVPN/Promise/Promise+ReceiveOn.swift
index a7ce8f039b..6979d5e3fb 100644
--- a/ios/MullvadVPN/Promise/Promise+ReceiveOn.swift
+++ b/ios/MullvadVPN/Promise/Promise+ReceiveOn.swift
@@ -9,24 +9,48 @@
import Foundation
extension Promise {
+ /// A type of timer.
+ enum TimerType {
+ case deadline
+ case walltime
+ }
+
/// Dispatch the upstream value on another queue.
func receive(on queue: DispatchQueue) -> Promise<Value> {
return Promise<Value> { resolver in
- _ = self.observe { completion in
- queue.async {
+ self.observe { completion in
+ let work = DispatchWorkItem {
resolver.resolve(completion: completion, queue: queue)
}
+
+ resolver.setCancelHandler {
+ work.cancel()
+ }
+
+ queue.async(execute: work)
}
}
}
/// Dispatch the upstream value on another queue after delay.
- func receive(on queue: DispatchQueue, after deadline: DispatchTime) -> Promise<Value> {
+ func receive(on queue: DispatchQueue, after timeInterval: DispatchTimeInterval, timerType: TimerType) -> Promise<Value> {
return Promise<Value> { resolver in
- _ = self.observe { completion in
- queue.asyncAfter(deadline: deadline) {
+ self.observe { completion in
+ let work = DispatchWorkItem {
resolver.resolve(completion: completion, queue: queue)
}
+
+ resolver.setCancelHandler {
+ work.cancel()
+ }
+
+ switch timerType {
+ case .deadline:
+ queue.asyncAfter(deadline: .now() + timeInterval, execute: work)
+
+ case .walltime:
+ queue.asyncAfter(wallDeadline: .now() + timeInterval, execute: work)
+ }
}
}
}
diff --git a/ios/MullvadVPN/Promise/Promise+Result.swift b/ios/MullvadVPN/Promise/Promise+Result.swift
index 94dd511227..e1f0587d4c 100644
--- a/ios/MullvadVPN/Promise/Promise+Result.swift
+++ b/ios/MullvadVPN/Promise/Promise+Result.swift
@@ -51,21 +51,19 @@ extension Promise where Value: AnyResult {
}
}
- /// Perform actiion on success.
- func onSuccess(_ onResolve: @escaping (Success) -> Void) -> Self {
- return observe { completion in
- if case .success(let value) = completion.unwrappedValue?.asConcreteType() {
- onResolve(value)
- }
+ /// Perform action on success.
+ func onSuccess(_ onResolve: @escaping (Success) -> Void) -> Result<Success, Failure>.Promise {
+ return map { value -> Success in
+ onResolve(value)
+ return value
}
}
/// Perform action on failure.
- func onFailure(_ onResolve: @escaping (Failure) -> Void) -> Self {
- return observe { completion in
- if case .failure(let error) = completion.unwrappedValue?.asConcreteType() {
- onResolve(error)
- }
+ func onFailure(_ onResolve: @escaping (Failure) -> Void) -> Result<Success, Failure>.Promise {
+ return mapError { error -> Failure in
+ onResolve(error)
+ return error
}
}
@@ -98,7 +96,19 @@ extension Promise where Value: AnyResult {
/// Map failure to Result. Passes successful result downstream.
func flatMapError<NewFailure>(_ transform: @escaping (Failure) -> Result<Success, NewFailure>) -> Result<Success, NewFailure>.Promise {
return then { result in
- result.asConcreteType().flatMapError(transform)
+ return result.asConcreteType().flatMapError(transform)
+ }
+ }
+
+ /// Map failure to Result producing Promise. Passes successful result downstream.
+ func flatMapErrorThen<NewFailure>(_ transform: @escaping (Failure) -> Result<Success, NewFailure>.Promise) -> Result<Success, NewFailure>.Promise {
+ return then { result in
+ switch result.asConcreteType() {
+ case .success(let value):
+ return .success(value)
+ case .failure(let error):
+ return transform(error)
+ }
}
}
}
@@ -134,3 +144,10 @@ extension Result {
}
}
}
+
+extension Result where Success: AnyOptional {
+ /// Same as `value` except it flattens `T??` producing single Optional (`T?`)
+ var flattenValue: Success.Wrapped? {
+ return value?.asConcreteType().flatMap { $0 }
+ }
+}
diff --git a/ios/MullvadVPN/Promise/Promise.swift b/ios/MullvadVPN/Promise/Promise.swift
index 1e8d75be4f..df78aced85 100644
--- a/ios/MullvadVPN/Promise/Promise.swift
+++ b/ios/MullvadVPN/Promise/Promise.swift
@@ -26,6 +26,13 @@ final class Promise<Value> {
return Self.init(value: value)
}
+ /// Returns Promise with lazily resolved value.
+ class func deferred(_ producer: @escaping () -> Value) -> Self {
+ return Self.init { resolver in
+ resolver.resolve(value: producer())
+ }
+ }
+
/// Initialize Promise with the execution block.
init(body: @escaping (PromiseResolver<Value>) -> Void) {
state = .pending(body, nil)
@@ -38,17 +45,16 @@ final class Promise<Value> {
deinit {
switch state {
- case .resolved, .cancelled:
+ case .resolved, .cancelled, .pending:
break
- case .pending, .executing:
+ case .executing:
preconditionFailure("\(Self.self) is deallocated in \(state) state without being resolved or cancelled.")
}
}
/// Observe the result of Promise.
/// This method starts the promise execution if it hasn't started yet.
- @discardableResult
- func observe(_ receiveCompletion: @escaping (PromiseCompletion<Value>) -> Void) -> Self {
+ func observe(_ receiveCompletion: @escaping (PromiseCompletion<Value>) -> Void) {
return lock.withCriticalBlock {
switch state {
case .resolved(let value, let queue):
@@ -65,7 +71,6 @@ final class Promise<Value> {
case .executing:
observers.append(AnyPromiseObserver<Value>(receiveCompletion))
}
- return self
}
}
@@ -90,10 +95,10 @@ final class Promise<Value> {
/// Trasform the value by producing a promise.
func then<NewValue>(_ onResolve: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> {
return Promise<NewValue> { resolver in
- _ = self.observe { completion in
+ self.observe { completion in
switch completion {
case .finished(let value):
- _ = onResolve(value).observe { completion in
+ onResolve(value).observe { completion in
resolver.resolve(completion: completion)
}
case .cancelled:
@@ -106,7 +111,7 @@ final class Promise<Value> {
/// Transform the value.
func then<NewValue>(_ onResolve: @escaping (Value) -> NewValue) -> Promise<NewValue> {
return Promise<NewValue> { resolver in
- _ = self.observe { completion in
+ self.observe { completion in
resolver.resolve(completion: completion.map(onResolve))
}
}
@@ -152,7 +157,7 @@ final class Promise<Value> {
defer { condition.unlock() }
var returnValue: PromiseCompletion<Value>!
- _ = observe { completion in
+ observe { completion in
returnValue = completion
condition.signal()
}
@@ -203,17 +208,27 @@ final class Promise<Value> {
}
final class PromiseCancellationToken {
- private let handler: () -> Void
+ private var handler: (() -> Void)?
+ private let lock = NSLock()
+
fileprivate init(_ handler: @escaping () -> Void) {
self.handler = handler
}
+ func cancel() {
+ lock.withCriticalBlock {
+ self.handler?()
+ self.handler = nil
+ }
+ }
+
deinit {
- handler()
+ cancel()
}
}
struct PromiseResolver<Value> {
+ /// Target promise.
private let promise: Promise<Value>
/// Private initializer.
@@ -250,7 +265,7 @@ struct PromiseResolver<Value> {
/// Set cancellation handler.
func setCancelHandler(_ cancellation: @escaping () -> Void) {
- _ = promise.observe { completion in
+ promise.observe { completion in
switch completion {
case .finished:
break
diff --git a/ios/MullvadVPN/Promise/PromiseCompletion.swift b/ios/MullvadVPN/Promise/PromiseCompletion.swift
index b424e29518..65499ed021 100644
--- a/ios/MullvadVPN/Promise/PromiseCompletion.swift
+++ b/ios/MullvadVPN/Promise/PromiseCompletion.swift
@@ -26,6 +26,16 @@ enum PromiseCompletion<Value> {
}
}
+ /// Returns `true` when the completion is `.cancelled`.
+ var isCancelled: Bool {
+ switch self {
+ case .cancelled:
+ return true
+ case .finished:
+ return false
+ }
+ }
+
/// Map the contained value, producing new `PromiseCompletion` type.
func map<NewValue>(_ transform: (Value) throws -> NewValue) rethrows -> PromiseCompletion<NewValue> {
switch self {
@@ -37,6 +47,13 @@ enum PromiseCompletion<Value> {
}
}
+extension PromiseCompletion where Value: AnyOptional {
+ /// Same as `unwrappedValue` except it flattens `T??` producing single Optional (`T?`)
+ var flattenUnwrappedValue: Value.Wrapped? {
+ return unwrappedValue?.asConcreteType().flatMap { $0 }
+ }
+}
+
extension PromiseCompletion: Equatable where Value: Equatable {
static func == (lhs: PromiseCompletion<Value>, rhs: PromiseCompletion<Value>) -> Bool {
switch (lhs, rhs) {
diff --git a/ios/MullvadVPNTests/PromiseTests.swift b/ios/MullvadVPNTests/PromiseTests.swift
index 616d492a3d..2c436d1ede 100644
--- a/ios/MullvadVPNTests/PromiseTests.swift
+++ b/ios/MullvadVPNTests/PromiseTests.swift
@@ -112,7 +112,9 @@ class PromiseTests: XCTestCase {
}
DispatchQueue.main.async(execute: work)
- }.observe { completion in
+ }
+
+ promise.observe { completion in
XCTAssertEqual(completion, .cancelled)
completionExpectation.fulfill()
}
@@ -144,4 +146,51 @@ class PromiseTests: XCTestCase {
}
}
+ func testRunOnOperationQueue() {
+ let operationQueue = OperationQueue()
+ operationQueue.name = "SerialOperationQueue"
+ operationQueue.maxConcurrentOperationCount = 1
+
+ let expect1 = expectation(description: "Wait for the first promise")
+ let expect2 = expectation(description: "Wait for the second promise")
+
+ Promise(value: 1)
+ .receive(on: .main, after: .milliseconds(100), timerType: .deadline)
+ .run(on: operationQueue)
+ .observe { completion in
+ expect1.fulfill()
+ }
+
+ Promise(value: 2)
+ .run(on: operationQueue)
+ .observe { completion in
+ expect2.fulfill()
+ }
+
+ wait(for: [expect1, expect2], timeout: 1)
+ }
+
+ func testRunOnOperationQueueWithExcusiveCategory() {
+ let operationQueue = OperationQueue()
+ operationQueue.name = "ConcurrentOperationQueue"
+
+ let expect1 = expectation(description: "Wait for the first promise")
+ let expect2 = expectation(description: "Wait for the second promise")
+
+ Promise(value: 1)
+ .receive(on: .main, after: .milliseconds(100), timerType: .deadline)
+ .run(on: operationQueue, categories: ["MutuallyExclusive"])
+ .observe { completion in
+ expect1.fulfill()
+ }
+
+ Promise(value: 2)
+ .run(on: operationQueue, categories: ["MutuallyExclusive"])
+ .observe { completion in
+ expect2.fulfill()
+ }
+
+ wait(for: [expect1, expect2], timeout: 1)
+ }
+
}