diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-09-15 11:01:42 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-09-15 11:01:42 +0200 |
| commit | d336fcb0f55336373e076745fc3394232d7ca44a (patch) | |
| tree | 3ce3aec7a58c069b55a12e0d8b3b4dfbea5a3636 /ios | |
| parent | 0637057254b2a9b24d29ce87df3c6f49d16b4cf6 (diff) | |
| parent | a63cba4a2d2eb237207fd77c4120dc13928df47e (diff) | |
| download | mullvadvpn-d336fcb0f55336373e076745fc3394232d7ca44a.tar.xz mullvadvpn-d336fcb0f55336373e076745fc3394232d7ca44a.zip | |
Merge branch 'promise-extensions'
Diffstat (limited to 'ios')
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 16 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/ExclusivityController.swift | 118 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise+BackgroundTask.swift | 57 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise+OperationQueue.swift | 49 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise+Optional.swift | 7 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise+ReceiveOn.swift | 34 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise+Result.swift | 41 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise.swift | 39 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/PromiseCompletion.swift | 17 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/PromiseTests.swift | 51 |
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) + } + } |
