diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-05-04 11:28:56 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-05-04 11:28:56 +0200 |
| commit | 6ed13e3a8ad510f2d2f751ffe57301252294160e (patch) | |
| tree | b538ac455cb39b51c849a6b0c52384e26b7db150 | |
| parent | 492b4572963f0ab42a3150bb92d36a1c495eb515 (diff) | |
| parent | b1b1c15666c08b1a2a3185b7f0a30befd67b98a8 (diff) | |
| download | mullvadvpn-6ed13e3a8ad510f2d2f751ffe57301252294160e.tar.xz mullvadvpn-6ed13e3a8ad510f2d2f751ffe57301252294160e.zip | |
Merge branch 'cancellable-initializer'
22 files changed, 531 insertions, 444 deletions
diff --git a/ios/MullvadREST/RESTAccessTokenManager.swift b/ios/MullvadREST/RESTAccessTokenManager.swift index d8d7ef5521..8b374b88d5 100644 --- a/ios/MullvadREST/RESTAccessTokenManager.swift +++ b/ios/MullvadREST/RESTAccessTokenManager.swift @@ -27,42 +27,34 @@ extension REST { accountNumber: String, completionHandler: @escaping (Result<REST.AccessTokenData, Swift.Error>) -> Void ) -> Cancellable { - let operation = ResultBlockOperation<REST.AccessTokenData>(dispatchQueue: dispatchQueue) + let operation = + ResultBlockOperation<REST.AccessTokenData>(dispatchQueue: dispatchQueue) { finish -> Cancellable in + if let tokenData = self.tokens[accountNumber], tokenData.expiry > Date() { + finish(.success(tokenData)) + return AnyCancellable() + } - operation.setExecutionBlock { operation in - if let tokenData = self.tokens[accountNumber], tokenData.expiry > Date() { - operation.finish(result: .success(tokenData)) - return - } + return self.proxy.getAccessToken(accountNumber: accountNumber, retryStrategy: .noRetry) { result in + self.dispatchQueue.async { + switch result { + case let .success(tokenData): + self.tokens[accountNumber] = tokenData - let task = self.proxy.getAccessToken( - accountNumber: accountNumber, - retryStrategy: .noRetry - ) { result in - self.dispatchQueue.async { - switch result { - case let .success(tokenData): - self.tokens[accountNumber] = tokenData + case let .failure(error) where !error.isOperationCancellationError: + self.logger.error( + error: error, + message: "Failed to fetch access token." + ) - case let .failure(error) where !error.isOperationCancellationError: - self.logger.error( - error: error, - message: "Failed to fetch access token." - ) + default: + break + } - default: - break + finish(result) } - - operation.finish(result: result) } } - operation.addCancellationBlock { - task.cancel() - } - } - operation.completionQueue = .main operation.completionHandler = completionHandler diff --git a/ios/MullvadTypes/Cancellable.swift b/ios/MullvadTypes/Cancellable.swift index a00b76eeda..8f658a6da1 100644 --- a/ios/MullvadTypes/Cancellable.swift +++ b/ios/MullvadTypes/Cancellable.swift @@ -13,3 +13,22 @@ public protocol Cancellable { } extension Operation: Cancellable {} + +/// An object representing a cancellation token. +public final class AnyCancellable: Cancellable { + private let block: (() -> Void)? + + /// Create cancellation token with block handler. + public init(block: @escaping () -> Void) { + self.block = block + } + + /// Create empty cancellation token. + public init() { + block = nil + } + + public func cancel() { + block?() + } +} diff --git a/ios/MullvadVPN/Extensions/Result+Extensions.swift b/ios/MullvadTypes/Result+Extensions.swift index 204df0714e..a471d9ab26 100644 --- a/ios/MullvadVPN/Extensions/Result+Extensions.swift +++ b/ios/MullvadTypes/Result+Extensions.swift @@ -9,7 +9,7 @@ import Foundation extension Result { - var value: Success? { + public var value: Success? { switch self { case let .success(value): return value @@ -18,7 +18,7 @@ extension Result { } } - var error: Failure? { + public var error: Failure? { switch self { case .success: return nil @@ -27,7 +27,7 @@ extension Result { } } - var isSuccess: Bool { + public var isSuccess: Bool { switch self { case .success: return true @@ -36,7 +36,7 @@ extension Result { } } - func tryMap<NewSuccess>(_ body: (Success) throws -> NewSuccess) -> Result<NewSuccess, Error> { + public func tryMap<NewSuccess>(_ body: (Success) throws -> NewSuccess) -> Result<NewSuccess, Error> { return Result<NewSuccess, Error> { let value = try self.get() @@ -44,7 +44,7 @@ extension Result { } } - @discardableResult func inspectError(_ body: (Failure) -> Void) -> Self { + @discardableResult public func inspectError(_ body: (Failure) -> Void) -> Self { if case let .failure(error) = self { body(error) } @@ -53,7 +53,7 @@ extension Result { } extension Result { - func flattenValue<T>() -> T? where Success == T? { + public func flattenValue<T>() -> T? where Success == T? { switch self { case let .success(optional): return optional.flatMap { $0 } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index d12da06daa..2dbb51e2b2 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -26,7 +26,6 @@ 06410E05292D0FC000AFC18C /* SettingsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410E03292D0F7100AFC18C /* SettingsParser.swift */; }; 06410E07292D108E00AFC18C /* SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410E06292D108E00AFC18C /* SettingsStore.swift */; }; 06410E08292D117800AFC18C /* SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410E06292D108E00AFC18C /* SettingsStore.swift */; }; - 06410E09292D990C00AFC18C /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */; }; 06799ACE28F98E1D00ACD94E /* MullvadREST.h in Headers */ = {isa = PBXBuildFile; fileRef = 06799ABE28F98E1D00ACD94E /* MullvadREST.h */; settings = {ATTRIBUTES = (Public, ); }; }; 06799AD128F98E1D00ACD94E /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; }; 06799AD228F98E1D00ACD94E /* MullvadREST.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -117,7 +116,6 @@ 585E820327F3285E00939F0E /* SendStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */; }; 58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */; }; 586168692976F6BD00EF8598 /* DisplayError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586168682976F6BD00EF8598 /* DisplayError.swift */; }; - 586250BB29E6F8F300F4B521 /* OperationCancellationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586250BA29E6F8F300F4B521 /* OperationCancellationTests.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; }; 5864211F29F04CED00822139 /* UIBarButtonItem+Blocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864211E29F04CED00822139 /* UIBarButtonItem+Blocks.swift */; }; 5864859929A0D028006C5743 /* FormsheetPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864859829A0D028006C5743 /* FormsheetPresentationController.swift */; }; @@ -222,6 +220,9 @@ 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */; }; 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */; }; 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */; }; + 58AFC99529F96F7B000829DE /* AsyncBlockOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99429F96F7B000829DE /* AsyncBlockOperationTests.swift */; }; + 58AFC99729F9753D000829DE /* AsyncResultBlockOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99629F9753D000829DE /* AsyncResultBlockOperationTests.swift */; }; + 58AFC99929F97856000829DE /* TransformOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99829F97856000829DE /* TransformOperationTests.swift */; }; 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; }; 58B26E1E2943514300D5980C /* InAppNotificationDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */; }; 58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E21294351EA00D5980C /* InAppNotificationProvider.swift */; }; @@ -335,13 +336,13 @@ 58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */; }; 58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E20770274672CA00DE5D77 /* LaunchViewController.swift */; }; 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */; }; + 58E45A5729F12C5100281ECF /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */; }; 58E511E628DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; }; 58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; }; 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */; }; 58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */; }; 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */; }; 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF581025D69DB400AEBA94 /* StatusImageView.swift */; }; - 58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */; }; 58F185AA298A3E3E00075977 /* TunnelCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F185A9298A3E3E00075977 /* TunnelCoordinator.swift */; }; 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */; }; 58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */; }; @@ -556,6 +557,13 @@ remoteGlobalIDString = 5898D28829017BD300EB5EBA; remoteInfo = TunnelProviderMessaging; }; + 58EED36D29FBEF040000CBAF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 58CE5E58224146200008646E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 58D223D4294C8E5E0029F5F8; + remoteInfo = MullvadTypes; + }; 58FBDAA122A52A6800EB69A3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; @@ -749,7 +757,6 @@ 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendStoreReceiptOperation.swift; sourceTree = "<group>"; }; 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryInAppNotificationProvider.swift; sourceTree = "<group>"; }; 586168682976F6BD00EF8598 /* DisplayError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayError.swift; sourceTree = "<group>"; }; - 586250BA29E6F8F300F4B521 /* OperationCancellationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationCancellationTests.swift; sourceTree = "<group>"; }; 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; }; 5864211E29F04CED00822139 /* UIBarButtonItem+Blocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Blocks.swift"; sourceTree = "<group>"; }; 5864859829A0D028006C5743 /* FormsheetPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormsheetPresentationController.swift; sourceTree = "<group>"; }; @@ -852,6 +859,9 @@ 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitch.swift; sourceTree = "<group>"; }; 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitchContainer.swift; sourceTree = "<group>"; }; 58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; }; + 58AFC99429F96F7B000829DE /* AsyncBlockOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperationTests.swift; sourceTree = "<group>"; }; + 58AFC99629F9753D000829DE /* AsyncResultBlockOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncResultBlockOperationTests.swift; sourceTree = "<group>"; }; + 58AFC99829F97856000829DE /* TransformOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformOperationTests.swift; sourceTree = "<group>"; }; 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppNotificationDescriptor.swift; sourceTree = "<group>"; }; @@ -1201,6 +1211,7 @@ 58900D0228BBDCC70094E4F0 /* FixedWidthInteger+Arithmetics.swift */, 06410E172934F43B00AFC18C /* PacketTunnelErrorWrapper.swift */, 58CAFA01298530DC00BE19F7 /* Promise.swift */, + 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, 58D223D7294C8E5E0029F5F8 /* MullvadTypes.h */, ); path = MullvadTypes; @@ -1432,7 +1443,6 @@ 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */, 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */, 58B9EB142489139B00095626 /* RESTError+Display.swift */, - 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, 58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */, 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */, 58A8EE5D2976DB00009C0F8D /* StorePaymentManagerError+Display.swift */, @@ -1643,11 +1653,13 @@ 589A455328E094B300565204 /* OperationsTests */ = { isa = PBXGroup; children = ( - 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */, + 58AFC99429F96F7B000829DE /* AsyncBlockOperationTests.swift */, + 58AFC99629F9753D000829DE /* AsyncResultBlockOperationTests.swift */, + 580CBFB72848D503007878F0 /* OperationConditionTests.swift */, 58DF5B772852178600E92647 /* OperationInputInjectionTests.swift */, 583E1E292848DF67004838B3 /* OperationObserverTests.swift */, - 580CBFB72848D503007878F0 /* OperationConditionTests.swift */, - 586250BA29E6F8F300F4B521 /* OperationCancellationTests.swift */, + 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */, + 58AFC99829F97856000829DE /* TransformOperationTests.swift */, ); path = OperationsTests; sourceTree = "<group>"; @@ -2198,6 +2210,7 @@ buildRules = ( ); dependencies = ( + 58EED36E29FBEF040000CBAF /* PBXTargetDependency */, ); name = Operations; productName = Operations; @@ -2568,9 +2581,11 @@ files = ( 589A455F28E094BF00565204 /* OperationConditionTests.swift in Sources */, 589A455E28E094BF00565204 /* OperationInputInjectionTests.swift in Sources */, - 586250BB29E6F8F300F4B521 /* OperationCancellationTests.swift in Sources */, + 58AFC99529F96F7B000829DE /* AsyncBlockOperationTests.swift in Sources */, + 58AFC99729F9753D000829DE /* AsyncResultBlockOperationTests.swift in Sources */, 589A455C28E094BF00565204 /* OperationSmokeTests.swift in Sources */, 589A455D28E094BF00565204 /* OperationObserverTests.swift in Sources */, + 58AFC99929F97856000829DE /* TransformOperationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2627,7 +2642,6 @@ 5878A27B2909649A0096FC88 /* CustomOverlayRenderer.swift in Sources */, 5847D58D29B7740F008C3808 /* RevokedCoordinator.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, - 58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */, 5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */, 58F185AA298A3E3E00075977 /* TunnelCoordinator.swift in Sources */, 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */, @@ -2795,7 +2809,6 @@ files = ( 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */, 587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */, - 06410E09292D990C00AFC18C /* Result+Extensions.swift in Sources */, 5893C6FA29C1B481009090D1 /* DNSSettings.swift in Sources */, 58CE38C828992C9200A6D6E5 /* TunnelMonitorDelegate.swift in Sources */, 068CE5782927BE4800A068BB /* Migration.swift in Sources */, @@ -2863,6 +2876,7 @@ 58D22408294C90210029F5F8 /* AnyIPEndpoint.swift in Sources */, 58D22409294C90210029F5F8 /* AnyIPAddress.swift in Sources */, 58D2240A294C90210029F5F8 /* IPAddress+Codable.swift in Sources */, + 58E45A5729F12C5100281ECF /* Result+Extensions.swift in Sources */, 58D2240B294C90210029F5F8 /* Cancellable.swift in Sources */, 58D2240C294C90210029F5F8 /* WrappingError.swift in Sources */, 58D2240D294C90210029F5F8 /* CustomErrorDescriptionProtocol.swift in Sources */, @@ -3043,6 +3057,11 @@ target = 5898D28829017BD300EB5EBA /* TunnelProviderMessaging */; targetProxy = 58D22433294C94890029F5F8 /* PBXContainerItemProxy */; }; + 58EED36E29FBEF040000CBAF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 58D223D4294C8E5E0029F5F8 /* MullvadTypes */; + targetProxy = 58EED36D29FBEF040000CBAF /* PBXContainerItemProxy */; + }; 58FBDAA222A52A6800EB69A3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 58FBDA9722A519BC00EB69A3 /* WireGuardGoBridge */; diff --git a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift index 8c3100f92b..d7c402c802 100644 --- a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift +++ b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift @@ -85,20 +85,16 @@ final class AddressCacheTracker { } func updateEndpoints(completionHandler: ((Result<Bool, Error>) -> Void)? = nil) -> Cancellable { - let operation = ResultBlockOperation<Bool> { operation in + let operation = ResultBlockOperation<Bool> { finish -> Cancellable in guard self.nextScheduleDate() <= Date() else { - operation.finish(result: .success(false)) - return + finish(.success(false)) + return AnyCancellable() } - let task = self.apiProxy.getAddressList(retryStrategy: .default) { result in + return self.apiProxy.getAddressList(retryStrategy: .default) { result in self.setEndpoints(from: result) - operation.finish(result: result.map { _ in true }) - } - - operation.addCancellationBlock { - task.cancel() + finish(result.map { _ in true }) } } diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 640d7fc86c..233c17d73f 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -370,7 +370,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func startInitialization(application: UIApplication) { let wipeSettingsOperation = getWipeSettingsOperation() - let loadTunnelStoreOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in + let loadTunnelStoreOperation = AsyncBlockOperation(dispatchQueue: .main) { finish in self.tunnelStore.loadPersistentTunnels { error in if let error = error { self.logger.error( @@ -378,16 +378,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD message: "Failed to load persistent tunnels." ) } - operation.finish() + finish(nil) } } - let migrateSettingsOperation = ResultBlockOperation<SettingsMigrationResult>( - dispatchQueue: .main - ) { operation in + let migrateSettingsOperation = ResultBlockOperation<SettingsMigrationResult>(dispatchQueue: .main) { finish in SettingsManager.migrateStore(with: self.proxyFactory) { migrationResult in let finishHandler = { - operation.finish(result: .success(migrationResult)) + finish(.success(migrationResult)) } guard case let .failure(error) = migrationResult, @@ -404,7 +402,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } migrateSettingsOperation.addDependencies([wipeSettingsOperation, loadTunnelStoreOperation]) - let initTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in + let initTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { finish in self.tunnelManager.loadConfiguration { error in // TODO: avoid throwing fatal error and show the problem report UI instead. if let error = error { @@ -416,7 +414,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD NotificationManager.shared.updateNotifications() self.storePaymentManager.startPaymentQueueMonitoring() - operation.finish() + finish(nil) } } initTunnelManagerOperation.addDependency(migrateSettingsOperation) diff --git a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift index 75b73b2119..402063052d 100644 --- a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift @@ -97,23 +97,16 @@ final class RelayCacheTracker { func updateRelays(completionHandler: ((Result<RelaysFetchResult, Error>) -> Void)? = nil) -> Cancellable { - let operation = ResultBlockOperation<RelaysFetchResult>(dispatchQueue: nil) { operation in + let operation = ResultBlockOperation<RelaysFetchResult> { finish in let cachedRelays = try? self.getCachedRelays() if self.getNextUpdateDate() > Date() { - operation.finish(result: .success(.throttled)) - return + finish(.success(.throttled)) + return AnyCancellable() } - let task = self.apiProxy.getRelays( - etag: cachedRelays?.etag, - retryStrategy: .noRetry - ) { result in - operation.finish(result: self.handleResponse(result: result)) - } - - operation.addCancellationBlock { - task.cancel() + return self.apiProxy.getRelays(etag: cachedRelays?.etag, retryStrategy: .noRetry) { result in + finish(self.handleResponse(result: result)) } } diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift index 40e1b6b7e3..62ff2f11f9 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift @@ -181,17 +181,12 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { accountNumber: String, completionHandler: @escaping (StorePaymentManagerError?) -> Void ) { - let accountOperation = ResultBlockOperation<REST.AccountData>(dispatchQueue: .main) { op in - let task = self.accountsProxy.getAccountData( + let accountOperation = ResultBlockOperation<REST.AccountData>(dispatchQueue: .main) { finish in + return self.accountsProxy.getAccountData( accountNumber: accountNumber, - retryStrategy: .default - ) { result in - op.finish(result: result) - } - - op.addCancellationBlock { - task.cancel() - } + retryStrategy: .default, + completion: finish + ) } accountOperation.addObserver(BackgroundObserver( diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index 4f99c86bdc..e13e4373fe 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -9,6 +9,7 @@ import Foundation import MullvadLogging import MullvadREST +import MullvadTypes import Operations import class WireGuardKitTypes.PrivateKey import class WireGuardKitTypes.PublicKey @@ -183,12 +184,10 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { } private func getCreateAccountOperation() -> ResultBlockOperation<StoredAccountData> { - let operation = ResultBlockOperation<StoredAccountData>(dispatchQueue: dispatchQueue) - - operation.setExecutionBlock { operation in + return ResultBlockOperation<StoredAccountData>(dispatchQueue: dispatchQueue) { finish -> Cancellable in self.logger.debug("Create new account...") - let task = self.accountsProxy.createAccount(retryStrategy: .default) { result in + return self.accountsProxy.createAccount(retryStrategy: .default) { result in let result = result.inspectError { error in guard !error.isOperationCancellationError else { return } @@ -206,55 +205,37 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { ) } - operation.finish(result: result) - } - - operation.addCancellationBlock { - task.cancel() + finish(result) } } - - return operation } - private func getExistingAccountOperation(accountNumber: String) - -> ResultOperation<StoredAccountData> - { - let operation = ResultBlockOperation<StoredAccountData>(dispatchQueue: dispatchQueue) - - operation.setExecutionBlock { operation in + private func getExistingAccountOperation(accountNumber: String) -> ResultOperation<StoredAccountData> { + return ResultBlockOperation<StoredAccountData>(dispatchQueue: dispatchQueue) { finish -> Cancellable in self.logger.debug("Request account data...") - let task = self.accountsProxy.getAccountData( - accountNumber: accountNumber, - retryStrategy: .default - ) { result in - let result = result.inspectError { error in - guard !error.isOperationCancellationError else { return } - - self.logger.error( - error: error, - message: "Failed to receive account data." - ) - }.map { accountData -> StoredAccountData in - self.logger.debug("Received account data.") + return self.accountsProxy + .getAccountData(accountNumber: accountNumber, retryStrategy: .default) { result in + let result = result.inspectError { error in + guard !error.isOperationCancellationError else { return } - return StoredAccountData( - identifier: accountData.id, - number: accountNumber, - expiry: accountData.expiry - ) - } + self.logger.error( + error: error, + message: "Failed to receive account data." + ) + }.map { accountData -> StoredAccountData in + self.logger.debug("Received account data.") - operation.finish(result: result) - } + return StoredAccountData( + identifier: accountData.id, + number: accountNumber, + expiry: accountData.expiry + ) + } - operation.addCancellationBlock { - task.cancel() - } + finish(result) + } } - - return operation } private func getDeleteDeviceOperation() -> AsyncBlockOperation? { @@ -262,12 +243,10 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { return nil } - let operation = AsyncBlockOperation(dispatchQueue: dispatchQueue) - - operation.setExecutionBlock { operation in + let operation = AsyncBlockOperation(dispatchQueue: dispatchQueue) { finish -> Cancellable in self.logger.debug("Delete current device...") - let task = self.devicesProxy.deleteDevice( + return self.devicesProxy.deleteDevice( accountNumber: accountData.number, identifier: deviceData.identifier, retryStrategy: .default @@ -286,11 +265,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { break } - operation.finish(error: result.error) - } - - operation.addCancellationBlock { - task.cancel() + finish(result.error) } } @@ -298,7 +273,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { } private func getUnsetDeviceStateOperation() -> AsyncBlockOperation { - return AsyncBlockOperation(dispatchQueue: dispatchQueue) { operation in + return AsyncBlockOperation(dispatchQueue: dispatchQueue) { finish in // Tell the caller to unsubscribe from VPN status notifications. self.interactor.prepareForVPNConfigurationDeletion() @@ -311,7 +286,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { // Finish immediately if tunnel provider is not set. guard let tunnel = self.interactor.tunnel else { - operation.finish() + finish(nil) return } @@ -328,21 +303,17 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { self.interactor.setTunnel(nil, shouldRefreshTunnelState: false) - operation.finish() + finish(nil) } } } } - private func getCreateDeviceOperation() - -> TransformOperation<StoredAccountData, (PrivateKey, REST.Device)> - { - let createDeviceOperation = TransformOperation< - StoredAccountData, - (PrivateKey, REST.Device) - >(dispatchQueue: dispatchQueue) - - createDeviceOperation.setExecutionBlock { storedAccountData, operation in + private func getCreateDeviceOperation() -> TransformOperation<StoredAccountData, (PrivateKey, REST.Device)> { + return TransformOperation<StoredAccountData, ( + PrivateKey, + REST.Device + )>(dispatchQueue: dispatchQueue) { storedAccountData, finish -> Cancellable in self.logger.debug("Store last used account.") do { @@ -363,7 +334,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { hijackDNS: false ) - let task = self.devicesProxy.createDevice( + return self.devicesProxy.createDevice( accountNumber: storedAccountData.number, request: request, retryStrategy: .default @@ -376,26 +347,13 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { self.logger.error(error: error, message: "Failed to create device.") } - operation.finish(result: result) - } - - operation.addCancellationBlock { - task.cancel() + finish(result) } } - - return createDeviceOperation } - private func getSaveSettingsOperation() - -> TransformOperation<SetAccountResult, StoredAccountData> - { - let saveSettingsOperation = TransformOperation< - SetAccountResult, - StoredAccountData - >(dispatchQueue: dispatchQueue) - - saveSettingsOperation.setExecutionBlock { input in + private func getSaveSettingsOperation() -> TransformOperation<SetAccountResult, StoredAccountData> { + return TransformOperation<SetAccountResult, StoredAccountData>(dispatchQueue: dispatchQueue) { input in self.logger.debug("Saving settings...") let device = input.device @@ -421,7 +379,5 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { return input.accountData } - - return saveSettingsOperation } } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 9c9190200b..b2644dcec0 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -268,24 +268,21 @@ final class TunnelManager: StorePaymentObserver { } func reconnectTunnel(selectNewRelay: Bool, completionHandler: ((Error?) -> Void)? = nil) { - let operation = AsyncBlockOperation(dispatchQueue: internalQueue) { operation in - guard let tunnel = self.tunnel else { - operation.finish(error: UnsetTunnelError()) - return - } - + let operation = AsyncBlockOperation(dispatchQueue: internalQueue) { finish -> Cancellable in do { - let selectorResult = selectNewRelay ? try self.selectRelay() : nil - - let task = tunnel.reconnectTunnel(relaySelectorResult: selectorResult) { result in - operation.finish(error: result.error) + guard let tunnel = self.tunnel else { + throw UnsetTunnelError() } - operation.addCancellationBlock { - task.cancel() + let selectorResult = selectNewRelay ? try self.selectRelay() : nil + + return tunnel.reconnectTunnel(relaySelectorResult: selectorResult) { result in + finish(result.error) } } catch { - operation.finish(error: error) + finish(error) + + return AnyCancellable() } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/MapViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/MapViewController.swift index 243672597a..bff8888133 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/MapViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/MapViewController.swift @@ -234,9 +234,9 @@ final class MapViewController: UIViewController, MKMapViewDelegate { cancelOtherAnimations: Bool, block: @escaping (_ finish: @escaping () -> Void) -> Void ) { - let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in + let operation = AsyncBlockOperation(dispatchQueue: .main) { finish in block { - operation.finish() + finish(nil) } } diff --git a/ios/Operations/AsyncBlockOperation.swift b/ios/Operations/AsyncBlockOperation.swift index 43be9b8275..062fc528f5 100644 --- a/ios/Operations/AsyncBlockOperation.swift +++ b/ios/Operations/AsyncBlockOperation.swift @@ -7,78 +7,53 @@ // import Foundation +import protocol MullvadTypes.Cancellable /// Asynchronous block operation public class AsyncBlockOperation: AsyncOperation { - private var executionBlock: ((AsyncBlockOperation) -> Void)? - private var cancellationBlocks: [() -> Void] = [] + private var executor: ((@escaping (Error?) -> Void) -> Cancellable?)? + private var cancellableTask: Cancellable? - override public init(dispatchQueue: DispatchQueue? = nil) { - super.init(dispatchQueue: dispatchQueue) - } - - public init( - dispatchQueue: DispatchQueue? = nil, - block: @escaping (AsyncBlockOperation) -> Void - ) { - executionBlock = block + public init(dispatchQueue: DispatchQueue? = nil, block: @escaping (@escaping (Error?) -> Void) -> Void) { super.init(dispatchQueue: dispatchQueue) + executor = { finish in + block(finish) + return nil + } } public init(dispatchQueue: DispatchQueue? = nil, block: @escaping () -> Void) { - executionBlock = { operation in + super.init(dispatchQueue: dispatchQueue) + executor = { finish in block() - operation.finish() + finish(nil) + return nil } + } + + public init( + dispatchQueue: DispatchQueue? = nil, + cancellableTask: @escaping (@escaping (Error?) -> Void) -> Cancellable + ) { super.init(dispatchQueue: dispatchQueue) + executor = { cancellableTask($0) } } override public func main() { - let block = executionBlock - executionBlock = nil + let executor = executor + self.executor = nil - if let block = block { - block(self) - } else { - finish() - } + assert(executor != nil) + + cancellableTask = executor?(self.finish) } override public func operationDidCancel() { - let blocks = cancellationBlocks - cancellationBlocks.removeAll() - - for block in blocks { - block() - } + cancellableTask?.cancel() } override public func operationDidFinish() { - cancellationBlocks.removeAll() - executionBlock = nil - } - - public func setExecutionBlock(_ block: @escaping (AsyncBlockOperation) -> Void) { - dispatchQueue.async { - assert(!self.isExecuting && !self.isFinished) - self.executionBlock = block - } - } - - public func setExecutionBlock(_ block: @escaping () -> Void) { - setExecutionBlock { operation in - block() - operation.finish() - } - } - - public func addCancellationBlock(_ block: @escaping () -> Void) { - dispatchQueue.async { - if self.isCancelled, self.isExecuting { - block() - } else { - self.cancellationBlocks.append(block) - } - } + executor = nil + cancellableTask = nil } } diff --git a/ios/Operations/AsyncOperation.swift b/ios/Operations/AsyncOperation.swift index 2ca049030c..ca11e86d7e 100644 --- a/ios/Operations/AsyncOperation.swift +++ b/ios/Operations/AsyncOperation.swift @@ -436,6 +436,11 @@ extension OperationBlockObserverSupport where Self: AsyncOperation { addBlockObserver(OperationBlockObserver(didFinish: fn)) } + /// Add observer responding to start event. + public func onStart(_ fn: @escaping (Self) -> Void) { + addBlockObserver(OperationBlockObserver(didStart: fn)) + } + /// Add block-based observer. public func addBlockObserver(_ observer: OperationBlockObserver<Self>) { addObserver(observer) diff --git a/ios/Operations/ResultBlockOperation.swift b/ios/Operations/ResultBlockOperation.swift index 5d848b438d..867dee45cf 100644 --- a/ios/Operations/ResultBlockOperation.swift +++ b/ios/Operations/ResultBlockOperation.swift @@ -7,102 +7,53 @@ // import Foundation +import protocol MullvadTypes.Cancellable public final class ResultBlockOperation<Success>: ResultOperation<Success> { - public typealias ExecutionBlock = (ResultBlockOperation<Success>) -> Void - public typealias ThrowingExecutionBlock = () throws -> Success + private var executor: ((@escaping (Result<Success, Error>) -> Void) -> Cancellable?)? + private var cancellableTask: Cancellable? - private var executionBlock: ExecutionBlock? - private var cancellationBlocks: [() -> Void] = [] - - public convenience init( + public init( dispatchQueue: DispatchQueue? = nil, - executionBlock: ExecutionBlock? = nil + executionBlock: @escaping (_ finish: @escaping (Result<Success, Error>) -> Void) -> Void ) { - self.init( - dispatchQueue: dispatchQueue, - executionBlock: executionBlock, - completionQueue: nil, - completionHandler: nil - ) + super.init(dispatchQueue: dispatchQueue) + executor = { finish in + executionBlock(finish) + return nil + } } - public convenience init( - dispatchQueue: DispatchQueue? = nil, - executionBlock: @escaping ThrowingExecutionBlock - ) { - self.init( - dispatchQueue: dispatchQueue, - executionBlock: Self.wrapThrowingBlock(executionBlock), - completionQueue: nil, - completionHandler: nil - ) + public init(dispatchQueue: DispatchQueue? = nil, executionBlock: @escaping () throws -> Success) { + super.init(dispatchQueue: dispatchQueue) + executor = { finish in + finish(Result { try executionBlock() }) + return nil + } } public init( - dispatchQueue: DispatchQueue?, - executionBlock: ExecutionBlock?, - completionQueue: DispatchQueue?, - completionHandler: CompletionHandler? + dispatchQueue: DispatchQueue? = nil, + cancellableTask: @escaping (_ finish: @escaping (Result<Success, Error>) -> Void) -> Cancellable ) { - self.executionBlock = executionBlock - - super.init( - dispatchQueue: dispatchQueue, - completionQueue: completionQueue, - completionHandler: completionHandler - ) + super.init(dispatchQueue: dispatchQueue) + executor = { cancellableTask($0) } } override public func main() { - let block = executionBlock - executionBlock = nil + let executor = executor + self.executor = nil - block?(self) + assert(executor != nil) + cancellableTask = executor?(self.finish) } override public func operationDidCancel() { - let blocks = cancellationBlocks - cancellationBlocks.removeAll() - - for block in blocks { - block() - } + cancellableTask?.cancel() } override public func operationDidFinish() { - cancellationBlocks.removeAll() - executionBlock = nil - } - - public func setExecutionBlock(_ block: @escaping ExecutionBlock) { - dispatchQueue.async { - assert(!self.isExecuting && !self.isFinished) - self.executionBlock = block - } - } - - public func setExecutionBlock(_ block: @escaping ThrowingExecutionBlock) { - setExecutionBlock(Self.wrapThrowingBlock(block)) - } - - public func addCancellationBlock(_ block: @escaping () -> Void) { - dispatchQueue.async { - if self.isCancelled, self.isExecuting { - block() - } else { - self.cancellationBlocks.append(block) - } - } - } - - private class func wrapThrowingBlock(_ executionBlock: @escaping ThrowingExecutionBlock) - -> ExecutionBlock - { - return { operation in - let result = Result { try executionBlock() } - - operation.finish(result: result) - } + executor = nil + cancellableTask = nil } } diff --git a/ios/Operations/TransformOperation.swift b/ios/Operations/TransformOperation.swift index 609e07896c..b5cc36adad 100644 --- a/ios/Operations/TransformOperation.swift +++ b/ios/Operations/TransformOperation.swift @@ -7,10 +7,9 @@ // import Foundation +import protocol MullvadTypes.Cancellable public final class TransformOperation<Input, Output>: ResultOperation<Output>, InputOperation { - public typealias ExecutionBlock = (Input, TransformOperation<Input, Output>) -> Void - public typealias ThrowingExecutionBlock = (Input) throws -> Output public typealias InputBlock = () -> Input? private let nslock = NSLock() @@ -35,79 +34,70 @@ public final class TransformOperation<Input, Output>: ResultOperation<Output>, I private var inputBlock: InputBlock? - private var executionBlock: ExecutionBlock? - private var cancellationBlocks: [() -> Void] = [] + private var executor: ((Input, @escaping (Result<Output, Error>) -> Void) -> Cancellable?)? + private var cancellableTask: Cancellable? public init( dispatchQueue: DispatchQueue? = nil, input: Input? = nil, - block: ExecutionBlock? = nil + block: @escaping (_ input: Input, _ finish: @escaping (Result<Output, Error>) -> Void) -> Void ) { - __input = input - executionBlock = block - super.init(dispatchQueue: dispatchQueue) + __input = input + executor = { input, finish in + block(input, finish) + return nil + } } public init( dispatchQueue: DispatchQueue? = nil, input: Input? = nil, - throwingBlock: @escaping ThrowingExecutionBlock + throwingBlock: @escaping (_ input: Input) throws -> Output ) { + super.init(dispatchQueue: dispatchQueue) __input = input - executionBlock = Self.wrapThrowingBlock(throwingBlock) + executor = { input, finish in + finish(Result { try throwingBlock(input) }) + return nil + } + } + public init( + dispatchQueue: DispatchQueue? = nil, + input: Input? = nil, + cancellableTask: @escaping (_ input: Input, _ finish: @escaping (Result<Output, Error>) -> Void) -> Cancellable + ) { super.init(dispatchQueue: dispatchQueue) + __input = input + executor = cancellableTask } override public func main() { - let inputValue = inputBlock?() - - _input = inputValue + if let inputBlock = inputBlock { + _input = inputBlock() + } - guard let inputValue = inputValue, let executionBlock = executionBlock else { + guard let inputValue = _input else { finish(result: .failure(OperationError.unsatisfiedRequirement)) return } - executionBlock(inputValue, self) - } + let executor = executor + self.executor = nil - override public func operationDidCancel() { - let blocks = cancellationBlocks - cancellationBlocks.removeAll() + assert(executor != nil) - for block in blocks { - block() - } + cancellableTask = executor?(inputValue, self.finish) } - override public func operationDidFinish() { - cancellationBlocks.removeAll() - executionBlock = nil - } - - // MARK: - Block handlers - - public func setExecutionBlock(_ block: @escaping ExecutionBlock) { - dispatchQueue.async { - assert(!self.isExecuting && !self.isFinished) - self.executionBlock = block - } - } - - public func setExecutionBlock(_ block: @escaping ThrowingExecutionBlock) { - setExecutionBlock(Self.wrapThrowingBlock(block)) + override public func operationDidCancel() { + cancellableTask?.cancel() } - public func addCancellationBlock(_ block: @escaping () -> Void) { - dispatchQueue.async { - if self.isCancelled, self.isExecuting { - block() - } else { - self.cancellationBlocks.append(block) - } - } + override public func operationDidFinish() { + executor = nil + cancellableTask = nil } // MARK: - Input injection @@ -117,14 +107,4 @@ public final class TransformOperation<Input, Output>: ResultOperation<Output>, I self.inputBlock = block } } - - private class func wrapThrowingBlock(_ executionBlock: @escaping ThrowingExecutionBlock) - -> ExecutionBlock - { - return { input, operation in - let result = Result { try executionBlock(input) } - - operation.finish(result: result) - } - } } diff --git a/ios/OperationsTests/AsyncBlockOperationTests.swift b/ios/OperationsTests/AsyncBlockOperationTests.swift new file mode 100644 index 0000000000..07a65d0ccd --- /dev/null +++ b/ios/OperationsTests/AsyncBlockOperationTests.swift @@ -0,0 +1,99 @@ +// +// AsyncBlockOperationTests.swift +// OperationsTests +// +// Created by pronebird on 26/04/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import MullvadTypes +import Operations +import XCTest + +final class AsyncBlockOperationTests: XCTestCase { + let operationQueue = AsyncOperationQueue() + + func testBlockOperation() { + let executionExpectation = expectation(description: "Should execute") + let finishExpectation = expectation(description: "Should finish") + + let operation = AsyncBlockOperation(block: { finish in + executionExpectation.fulfill() + finish(nil) + }) + + operation.completionBlock = { + finishExpectation.fulfill() + } + + operationQueue.addOperation(operation) + + waitForExpectations(timeout: 1) + } + + func testSynchronousBlockOperation() { + let executionExpectation = expectation(description: "Should execute") + let finishExpectation = expectation(description: "Should finish") + + let operation = AsyncBlockOperation { + executionExpectation.fulfill() + } + + operation.completionBlock = { + finishExpectation.fulfill() + } + + operationQueue.addOperation(operation) + + waitForExpectations(timeout: 1) + } + + func testCancellableTaskBlockOperation() { + let executionExpectation = expectation(description: "Should execute") + let cancelExpectation = expectation(description: "Should cancel") + let finishExpectation = expectation(description: "Should finish") + + let operation = AsyncBlockOperation { finish -> Cancellable in + executionExpectation.fulfill() + + return AnyCancellable { + cancelExpectation.fulfill() + finish(nil) + } + } + + operation.completionBlock = { + finishExpectation.fulfill() + } + + operation.onStart { op in + op.cancel() + } + + operationQueue.addOperation(operation) + + waitForExpectations(timeout: 1) + } + + func testCancellationShouldNotFireBeforeOperationIsEnqueued() throws { + let expect = expectation(description: "Cancellation should not fire.") + expect.isInverted = true + + let operation = AsyncBlockOperation {} + operation.onCancel { _ in expect.fulfill() } + operation.cancel() + + waitForExpectations(timeout: 1) + } + + func testCancellationShouldFireAfterCancelledOperationIsEnqueued() throws { + let expect = expectation(description: "Cancellation should fire.") + + let operation = AsyncBlockOperation {} + operation.onCancel { _ in expect.fulfill() } + operation.cancel() + operationQueue.addOperation(operation) + + waitForExpectations(timeout: 1) + } +} diff --git a/ios/OperationsTests/AsyncResultBlockOperationTests.swift b/ios/OperationsTests/AsyncResultBlockOperationTests.swift new file mode 100644 index 0000000000..5bae7f7d9c --- /dev/null +++ b/ios/OperationsTests/AsyncResultBlockOperationTests.swift @@ -0,0 +1,75 @@ +// +// AsyncResultBlockOperationTests.swift +// OperationsTests +// +// Created by pronebird on 26/04/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import MullvadTypes +import Operations +import XCTest + +final class AsyncResultBlockOperationTests: XCTestCase { + let operationQueue = AsyncOperationQueue() + + func testBlockOperation() { + let expectation = expectation(description: "Should finish") + + let operation = ResultBlockOperation<Bool> { finish in + finish(.success(true)) + } + + operation.onFinish { op, error in + XCTAssertEqual(op.result?.value, true) + expectation.fulfill() + } + + operationQueue.addOperation(operation) + + waitForExpectations(timeout: 1) + } + + func testThrowingBlockOperation() { + let expectation = expectation(description: "Should finish") + + let operation = ResultBlockOperation { + throw URLError(.badURL) + } + + operation.onFinish { op, error in + XCTAssertEqual(op.result?.error as? URLError, URLError(.badURL)) + XCTAssertEqual(error as? URLError, URLError(.badURL)) + + expectation.fulfill() + } + + operationQueue.addOperation(operation) + + waitForExpectations(timeout: 1) + } + + func testCancellableTaskOperation() { + let expectation = expectation(description: "Should finish") + + let operation = ResultBlockOperation<Bool> { finish -> Cancellable in + return AnyCancellable { + finish(.failure(URLError(.cancelled))) + } + } + + operation.onStart { op in + op.cancel() + } + + operation.onFinish { op, error in + XCTAssertEqual(op.result?.error as? URLError, URLError(.cancelled)) + XCTAssertEqual(error as? URLError, URLError(.cancelled)) + expectation.fulfill() + } + + operationQueue.addOperation(operation) + + waitForExpectations(timeout: 1) + } +} diff --git a/ios/OperationsTests/OperationCancellationTests.swift b/ios/OperationsTests/OperationCancellationTests.swift deleted file mode 100644 index 7313bc83b2..0000000000 --- a/ios/OperationsTests/OperationCancellationTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// OperationCancellationTests.swift -// OperationsTests -// -// Created by pronebird on 12/04/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Operations -import XCTest - -final class OperationCancellationTests: XCTestCase { - func testCancellationShouldNotFireBeforeOperationIsEnqueued() throws { - let expect = expectation(description: "Cancellation should not fire.") - expect.isInverted = true - - let operation = AsyncBlockOperation {} - operation.onCancel { _ in expect.fulfill() } - operation.cancel() - - waitForExpectations(timeout: 1) - } - - func testCancellationShouldFireAfterCancelledOperationIsEnqueued() throws { - let expect = expectation(description: "Cancellation should fire.") - - let operationQueue = AsyncOperationQueue() - let operation = AsyncBlockOperation {} - operation.onCancel { _ in expect.fulfill() } - operation.cancel() - operationQueue.addOperation(operation) - - waitForExpectations(timeout: 1) - } -} diff --git a/ios/OperationsTests/OperationConditionTests.swift b/ios/OperationsTests/OperationConditionTests.swift index b35e687319..27898ea424 100644 --- a/ios/OperationsTests/OperationConditionTests.swift +++ b/ios/OperationsTests/OperationConditionTests.swift @@ -123,10 +123,10 @@ class OperationConditionTests: XCTestCase { let exclusiveCategory = "exclusiveOperations" let operationQueue = AsyncOperationQueue() - let firstOperation = AsyncBlockOperation { op in + let firstOperation = AsyncBlockOperation { finish in DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { expectFirstOperationExecution.fulfill() - op.finish() + finish(nil) } } firstOperation.addCondition(MutuallyExclusive(category: exclusiveCategory)) diff --git a/ios/OperationsTests/OperationObserverTests.swift b/ios/OperationsTests/OperationObserverTests.swift index c246cdc526..de334be21a 100644 --- a/ios/OperationsTests/OperationObserverTests.swift +++ b/ios/OperationsTests/OperationObserverTests.swift @@ -17,7 +17,7 @@ class OperationObserverTests: XCTestCase { expectDidCancel.isInverted = true let expectDidFinish = expectation(description: "didAttach handler") - let operation = AsyncBlockOperation() + let operation = AsyncBlockOperation {} operation.addBlockObserver(OperationBlockObserver( didAttach: { op in expectDidAttach.fulfill() @@ -44,7 +44,7 @@ class OperationObserverTests: XCTestCase { let expectDidCancel = expectation(description: "didCancel handler") let expectDidFinish = expectation(description: "didAttach handler") - let operation = AsyncBlockOperation() + let operation = AsyncBlockOperation {} operation.addBlockObserver(OperationBlockObserver( didAttach: { op in expectDidAttach.fulfill() diff --git a/ios/OperationsTests/TransformOperationTests.swift b/ios/OperationsTests/TransformOperationTests.swift new file mode 100644 index 0000000000..c16bcd3be1 --- /dev/null +++ b/ios/OperationsTests/TransformOperationTests.swift @@ -0,0 +1,93 @@ +// +// TransformOperationTests.swift +// OperationsTests +// +// Created by pronebird on 26/04/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import MullvadTypes +import Operations +import XCTest + +final class TransformOperationTests: XCTestCase { + let operationQueue = AsyncOperationQueue() + + func testBlockTransformOperation() { + let finishExpectation = expectation(description: "Should finish") + + let transform = TransformOperation(input: Int.zero) { input, finish in + finish(.success(input + 1)) + } + + transform.onFinish { op, error in + XCTAssertEqual(op.result?.value, 1) + + finishExpectation.fulfill() + } + + operationQueue.addOperation(transform) + + waitForExpectations(timeout: 1) + } + + func testThrowingBlockTransformOperation() { + let finishExpectation = expectation(description: "Should finish") + + let transform = TransformOperation(input: Int.zero) { value in + throw URLError(.badURL) + } + + transform.onFinish { op, error in + XCTAssertEqual(error as? URLError, URLError(.badURL)) + + finishExpectation.fulfill() + } + + operationQueue.addOperation(transform) + + waitForExpectations(timeout: 1) + } + + func testCancellableTaskBlockTransformOperation() { + let finishExpectation = expectation(description: "Should finish") + + let transform = TransformOperation<Int, Int>(input: Int.zero) { _, finish -> Cancellable in + return AnyCancellable { + finish(.failure(URLError(.cancelled))) + } + } + + transform.onStart { op in + op.cancel() + } + + transform.onFinish { op, error in + XCTAssertEqual(error as? URLError, URLError(.cancelled)) + + finishExpectation.fulfill() + } + + operationQueue.addOperation(transform) + + waitForExpectations(timeout: 1) + } + + func testShouldFailWithUnsatisfiedRequirement() { + let finishExpectation = expectation(description: "Should finish") + + let transform = TransformOperation<Int, Int> { input, finish in + finish(.success(input)) + } + + transform.onFinish { _, error in + XCTAssertEqual(error as? OperationError, .unsatisfiedRequirement) + + finishExpectation.fulfill() + } + + operationQueue.addOperation(transform) + + waitForExpectations(timeout: 1) + } +} diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 380293695b..c70d5e0d35 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -585,16 +585,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // Ignore all requests to reconnect once tunnel is preparing to stop. guard !isStopping else { return } - let blockOperation = AsyncBlockOperation(dispatchQueue: dispatchQueue) { operation in + let blockOperation = AsyncBlockOperation(dispatchQueue: dispatchQueue, block: { finish in if shouldStopTunnelMonitor { self.tunnelMonitor.stop() } self.reconnectTunnelInner(to: nextRelay) { error in completionHandler?(error) - operation.finish() + finish(nil) } - } + }) if let reconnectTunnelTask = reconnectTunnelTask { blockOperation.addDependency(reconnectTunnelTask) @@ -779,49 +779,28 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { operationQueue.addOperation(groupOperation) } - private func createGetAccountDataOperation(accountNumber: String) - -> ResultOperation<REST.AccountData> - { - let operation = ResultBlockOperation<REST.AccountData>( - dispatchQueue: dispatchQueue - ) - - operation.setExecutionBlock { operation in - let task = self.accountsProxy.getAccountData( + private func createGetAccountDataOperation(accountNumber: String) -> ResultOperation<REST.AccountData> { + return ResultBlockOperation<REST.AccountData>(dispatchQueue: dispatchQueue) { finish -> Cancellable in + return self.accountsProxy.getAccountData( accountNumber: accountNumber, - retryStrategy: .noRetry - ) { result in - operation.finish(result: result) - } - - operation.addCancellationBlock { - task.cancel() - } + retryStrategy: .noRetry, + completion: finish + ) } - - return operation } - private func createGetDeviceDataOperation(accountNumber: String, identifier: String) - -> ResultOperation<REST.Device> - { - let operation = ResultBlockOperation<REST.Device>(dispatchQueue: dispatchQueue) - - operation.setExecutionBlock { operation in - let task = self.devicesProxy.getDevice( + private func createGetDeviceDataOperation( + accountNumber: String, + identifier: String + ) -> ResultOperation<REST.Device> { + return ResultBlockOperation<REST.Device>(dispatchQueue: dispatchQueue) { finish -> Cancellable in + return self.devicesProxy.getDevice( accountNumber: accountNumber, identifier: identifier, - retryStrategy: .noRetry - ) { result in - operation.finish(result: result) - } - - operation.addCancellationBlock { - task.cancel() - } + retryStrategy: .noRetry, + completion: finish + ) } - - return operation } } |
