summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-07-06 17:50:29 +0200
committerAndrej Mihajlov <and@mullvad.net>2020-07-06 21:00:20 +0200
commitf4a58dc484eb1464857644e79d6e46b9b36d2f0f (patch)
treed67505f53d264a3846c1caee987828bf96add90e
parentd30a40c59918736dafb2631bd332c5ef81b5cecc (diff)
downloadmullvadvpn-f4a58dc484eb1464857644e79d6e46b9b36d2f0f.tar.xz
mullvadvpn-f4a58dc484eb1464857644e79d6e46b9b36d2f0f.zip
Add operations
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj98
-rw-r--r--ios/MullvadVPN/Operations/AnyOperationObserver.swift15
-rw-r--r--ios/MullvadVPN/Operations/AssociatedValue.swift31
-rw-r--r--ios/MullvadVPN/Operations/AsyncBlockOperation.swift26
-rw-r--r--ios/MullvadVPN/Operations/AsyncOperation.swift142
-rw-r--r--ios/MullvadVPN/Operations/DelayOperation.swift48
-rw-r--r--ios/MullvadVPN/Operations/ExclusivityController.swift69
-rw-r--r--ios/MullvadVPN/Operations/InputOperation.swift103
-rw-r--r--ios/MullvadVPN/Operations/OperationBlockObserver.swift33
-rw-r--r--ios/MullvadVPN/Operations/OperationObserver.swift17
-rw-r--r--ios/MullvadVPN/Operations/OperationProtocol.swift20
-rw-r--r--ios/MullvadVPN/Operations/OutputOperation.swift50
-rw-r--r--ios/MullvadVPN/Operations/ResultOperation.swift63
-rw-r--r--ios/MullvadVPN/Operations/RetryOperation.swift120
-rw-r--r--ios/MullvadVPN/Operations/TransformOperation.swift50
-rw-r--r--ios/MullvadVPN/Operations/TransformOperationObserver.swift39
16 files changed, 924 insertions, 0 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index fed0afca2b..307a474a1b 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -10,6 +10,34 @@
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; };
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
+ 580EE20124B321D500F9D8A1 /* OperationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20024B321D500F9D8A1 /* OperationProtocol.swift */; };
+ 580EE20224B321DB00F9D8A1 /* OperationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20024B321D500F9D8A1 /* OperationProtocol.swift */; };
+ 580EE20424B321EC00F9D8A1 /* OperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20324B321EC00F9D8A1 /* OperationObserver.swift */; };
+ 580EE20624B3222200F9D8A1 /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; };
+ 580EE20724B3222400F9D8A1 /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; };
+ 580EE20924B3224200F9D8A1 /* RetryOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20824B3224200F9D8A1 /* RetryOperation.swift */; };
+ 580EE20A24B3224200F9D8A1 /* RetryOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20824B3224200F9D8A1 /* RetryOperation.swift */; };
+ 580EE20C24B3225F00F9D8A1 /* DelayOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20B24B3225F00F9D8A1 /* DelayOperation.swift */; };
+ 580EE20D24B3225F00F9D8A1 /* DelayOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20B24B3225F00F9D8A1 /* DelayOperation.swift */; };
+ 580EE20F24B322E700F9D8A1 /* TransformOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20E24B322E700F9D8A1 /* TransformOperation.swift */; };
+ 580EE21024B322E700F9D8A1 /* TransformOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20E24B322E700F9D8A1 /* TransformOperation.swift */; };
+ 580EE21224B322FC00F9D8A1 /* ResultOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21124B322FC00F9D8A1 /* ResultOperation.swift */; };
+ 580EE21324B322FC00F9D8A1 /* ResultOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21124B322FC00F9D8A1 /* ResultOperation.swift */; };
+ 580EE21524B3231200F9D8A1 /* OperationBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21424B3231200F9D8A1 /* OperationBlockObserver.swift */; };
+ 580EE21624B3231200F9D8A1 /* OperationBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21424B3231200F9D8A1 /* OperationBlockObserver.swift */; };
+ 580EE21824B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21724B3235100F9D8A1 /* AnyOperationObserver.swift */; };
+ 580EE21924B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21724B3235100F9D8A1 /* AnyOperationObserver.swift */; };
+ 580EE21B24B3236900F9D8A1 /* InputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21A24B3236900F9D8A1 /* InputOperation.swift */; };
+ 580EE21C24B3236900F9D8A1 /* InputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21A24B3236900F9D8A1 /* InputOperation.swift */; };
+ 580EE21E24B3237F00F9D8A1 /* OutputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21D24B3237F00F9D8A1 /* OutputOperation.swift */; };
+ 580EE21F24B3237F00F9D8A1 /* OutputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21D24B3237F00F9D8A1 /* OutputOperation.swift */; };
+ 580EE22124B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22024B3240100F9D8A1 /* TransformOperationObserver.swift */; };
+ 580EE22224B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22024B3240100F9D8A1 /* TransformOperationObserver.swift */; };
+ 580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; };
+ 580EE22524B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; };
+ 580EE22624B3245600F9D8A1 /* OperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20324B321EC00F9D8A1 /* OperationObserver.swift */; };
+ 580EE22824B3289300F9D8A1 /* AssociatedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */; };
+ 580EE22924B3289300F9D8A1 /* AssociatedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */; };
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; };
581CBCE62296B97300727D7F /* ViewControllerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581CBCE52296B97300727D7F /* ViewControllerIdentifier.swift */; };
581CBCEC2298041B00727D7F /* SettingsAppVersionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581CBCEB2298041B00727D7F /* SettingsAppVersionCell.swift */; };
@@ -71,6 +99,7 @@
5888AD89227B18C40051EB06 /* RelayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD88227B18C40051EB06 /* RelayList.swift */; };
588AE72F2362001F009F9F2E /* MutuallyExclusive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588AE72E2362001F009F9F2E /* MutuallyExclusive.swift */; };
588AE730236200E2009F9F2E /* MutuallyExclusive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588AE72E2362001F009F9F2E /* MutuallyExclusive.swift */; };
+ 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; };
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */; };
5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; };
5896AE7F246ACE76005B36CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; };
@@ -138,6 +167,7 @@
58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */; };
58EC4E6C23915325003F5C5B /* Bundle+MullvadVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EC4E6B23915325003F5C5B /* Bundle+MullvadVersion.swift */; };
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */; };
+ 58F3C0962492617E003E76BE /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; };
58F840AF2464382C0044E708 /* KeychainItemRevision.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840AE2464382C0044E708 /* KeychainItemRevision.swift */; };
58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840AE2464382C0044E708 /* KeychainItemRevision.swift */; };
58F840B22464491D0044E708 /* ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840B12464491D0044E708 /* ChainedError.swift */; };
@@ -203,6 +233,20 @@
/* Begin PBXFileReference section */
5807E2BF2432038B00F5FF30 /* String+Split.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Split.swift"; sourceTree = "<group>"; };
5807E2C1243203D000F5FF30 /* StringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = "<group>"; };
+ 580EE20024B321D500F9D8A1 /* OperationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationProtocol.swift; sourceTree = "<group>"; };
+ 580EE20324B321EC00F9D8A1 /* OperationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationObserver.swift; sourceTree = "<group>"; };
+ 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExclusivityController.swift; sourceTree = "<group>"; };
+ 580EE20824B3224200F9D8A1 /* RetryOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryOperation.swift; sourceTree = "<group>"; };
+ 580EE20B24B3225F00F9D8A1 /* DelayOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayOperation.swift; sourceTree = "<group>"; };
+ 580EE20E24B322E700F9D8A1 /* TransformOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformOperation.swift; sourceTree = "<group>"; };
+ 580EE21124B322FC00F9D8A1 /* ResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultOperation.swift; sourceTree = "<group>"; };
+ 580EE21424B3231200F9D8A1 /* OperationBlockObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserver.swift; sourceTree = "<group>"; };
+ 580EE21724B3235100F9D8A1 /* AnyOperationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyOperationObserver.swift; sourceTree = "<group>"; };
+ 580EE21A24B3236900F9D8A1 /* InputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputOperation.swift; sourceTree = "<group>"; };
+ 580EE21D24B3237F00F9D8A1 /* OutputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputOperation.swift; sourceTree = "<group>"; };
+ 580EE22024B3240100F9D8A1 /* TransformOperationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformOperationObserver.swift; sourceTree = "<group>"; };
+ 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperation.swift; sourceTree = "<group>"; };
+ 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociatedValue.swift; sourceTree = "<group>"; };
5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+Debug.swift"; sourceTree = "<group>"; };
581CBCE52296B97300727D7F /* ViewControllerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerIdentifier.swift; sourceTree = "<group>"; };
581CBCEB2298041B00727D7F /* SettingsAppVersionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionCell.swift; sourceTree = "<group>"; };
@@ -300,6 +344,7 @@
58D0C79F23F1CECF00FE9BA7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MullvadVPNScreenshots.swift; sourceTree = "<group>"; };
58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManager.swift; sourceTree = "<group>"; };
+ 58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; };
58EC4E6B23915325003F5C5B /* Bundle+MullvadVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+MullvadVersion.swift"; sourceTree = "<group>"; };
58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = "<group>"; };
58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerActivityIndicatorView.swift; sourceTree = "<group>"; };
@@ -365,6 +410,28 @@
name = Frameworks;
sourceTree = "<group>";
};
+ 580EE1FF24B3218800F9D8A1 /* Operations */ = {
+ isa = PBXGroup;
+ children = (
+ 580EE21724B3235100F9D8A1 /* AnyOperationObserver.swift */,
+ 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */,
+ 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */,
+ 58E973DD24850EB600096F90 /* AsyncOperation.swift */,
+ 580EE20B24B3225F00F9D8A1 /* DelayOperation.swift */,
+ 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */,
+ 580EE21A24B3236900F9D8A1 /* InputOperation.swift */,
+ 580EE21424B3231200F9D8A1 /* OperationBlockObserver.swift */,
+ 580EE20324B321EC00F9D8A1 /* OperationObserver.swift */,
+ 580EE20024B321D500F9D8A1 /* OperationProtocol.swift */,
+ 580EE21D24B3237F00F9D8A1 /* OutputOperation.swift */,
+ 580EE21124B322FC00F9D8A1 /* ResultOperation.swift */,
+ 580EE20824B3224200F9D8A1 /* RetryOperation.swift */,
+ 580EE20E24B322E700F9D8A1 /* TransformOperation.swift */,
+ 580EE22024B3240100F9D8A1 /* TransformOperationObserver.swift */,
+ );
+ path = Operations;
+ sourceTree = "<group>";
+ };
58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = {
isa = PBXGroup;
children = (
@@ -457,6 +524,7 @@
588AE72E2362001F009F9F2E /* MutuallyExclusive.swift */,
58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */,
5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */,
+ 580EE1FF24B3218800F9D8A1 /* Operations */,
5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */,
58BFA5C522A7C97F00A6173D /* RelayCache.swift */,
58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */,
@@ -803,8 +871,10 @@
files = (
58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */,
58BA692E23E99EFF009DC256 /* Locking.swift in Sources */,
+ 580EE21E24B3237F00F9D8A1 /* OutputOperation.swift in Sources */,
5840250122B1124600E4CFEC /* IpAddress+Codable.swift in Sources */,
58EC4E6C23915325003F5C5B /* Bundle+MullvadVersion.swift in Sources */,
+ 580EE21224B322FC00F9D8A1 /* ResultOperation.swift in Sources */,
58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */,
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */,
58FD5BE92419406000112C88 /* SKRequestPublisher.swift in Sources */,
@@ -812,14 +882,18 @@
582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */,
58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */,
581CBCE62296B97300727D7F /* ViewControllerIdentifier.swift in Sources */,
+ 580EE21524B3231200F9D8A1 /* OperationBlockObserver.swift in Sources */,
58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */,
582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */,
+ 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */,
5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */,
58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */,
58C6B35422BB87C4003C19AD /* WireguardPrivateKey.swift in Sources */,
+ 580EE20924B3224200F9D8A1 /* RetryOperation.swift in Sources */,
582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */,
58FAEDF7245088E100CB0F5B /* Keychain.swift in Sources */,
5888AD87227B17950051EB06 /* SelectLocationController.swift in Sources */,
+ 580EE20424B321EC00F9D8A1 /* OperationObserver.swift in Sources */,
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */,
584E96BA240D791E00D3334F /* CancellableDelayPublisher.swift in Sources */,
58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */,
@@ -839,18 +913,22 @@
58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */,
584E96BC240FD4DA00D3334F /* Location.swift in Sources */,
58ADDB3E227B1CD900FAFEA7 /* MullvadRpc.swift in Sources */,
+ 580EE20F24B322E700F9D8A1 /* TransformOperation.swift in Sources */,
58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */,
587B08E0229433EB000E6F17 /* LoginState.swift in Sources */,
58C6B34F22BB7AC0003C19AD /* IPAddressRange.swift in Sources */,
58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */,
+ 580EE22124B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */,
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */,
5873884D239E6D7E00E96C4E /* EmbeddedViewContainerView.swift in Sources */,
582650862384116F00FA7A86 /* ReplaceNilWithError.swift in Sources */,
587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */,
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */,
+ 580EE20624B3222200F9D8A1 /* ExclusivityController.swift in Sources */,
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */,
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
+ 580EE21B24B3236900F9D8A1 /* InputOperation.swift in Sources */,
5877152E23981C5B001F8237 /* SettingsBasicCell.swift in Sources */,
58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */,
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */,
@@ -859,8 +937,10 @@
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
58C6B35E22BBBFE3003C19AD /* Data+HexCoding.swift in Sources */,
58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */,
+ 580EE22824B3289300F9D8A1 /* AssociatedValue.swift in Sources */,
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */,
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */,
+ 580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */,
589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */,
58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */,
5888AD7F2279B6BF0051EB06 /* RelayStatusIndicatorView.swift in Sources */,
@@ -871,10 +951,12 @@
58A8BE8323A0F362006B74AC /* UIAlertController+Error.swift in Sources */,
58F840AF2464382C0044E708 /* KeychainItemRevision.swift in Sources */,
587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */,
+ 580EE20124B321D500F9D8A1 /* OperationProtocol.swift in Sources */,
5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */,
588AE72F2362001F009F9F2E /* MutuallyExclusive.swift in Sources */,
5888AD89227B18C40051EB06 /* RelayList.swift in Sources */,
587AD7C623421D7000E93A53 /* TunnelConfiguration.swift in Sources */,
+ 580EE21824B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */,
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */,
58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */,
58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */,
@@ -882,6 +964,7 @@
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
58F840B22464491D0044E708 /* ChainedError.swift in Sources */,
58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */,
+ 580EE20C24B3225F00F9D8A1 /* DelayOperation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -891,19 +974,26 @@
files = (
5845F83C236C72E300B2D93C /* AutoDisposableSink.swift in Sources */,
5860F1C423A8D25F00CEA666 /* WireguardConfiguration.swift in Sources */,
+ 580EE21F24B3237F00F9D8A1 /* OutputOperation.swift in Sources */,
+ 580EE20224B321DB00F9D8A1 /* OperationProtocol.swift in Sources */,
58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */,
58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */,
58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */,
58BFA5C222A7C92900A6173D /* JsonRpc.swift in Sources */,
588AE730236200E2009F9F2E /* MutuallyExclusive.swift in Sources */,
+ 580EE20724B3222400F9D8A1 /* ExclusivityController.swift in Sources */,
58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */,
58C6B35122BB7CFD003C19AD /* IPAddressRange.swift in Sources */,
587AD7C723421D8600E93A53 /* TunnelConfiguration.swift in Sources */,
+ 58F3C0962492617E003E76BE /* AsyncOperation.swift in Sources */,
+ 580EE22924B3289300F9D8A1 /* AssociatedValue.swift in Sources */,
58BFA5C322A7C93400A6173D /* RelayList.swift in Sources */,
58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */,
587AD7C82342237300E93A53 /* TunnelManager.swift in Sources */,
5840250222B1124600E4CFEC /* IpAddress+Codable.swift in Sources */,
58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */,
+ 580EE21924B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */,
+ 580EE21324B322FC00F9D8A1 /* ResultOperation.swift in Sources */,
58C6B36522C10596003C19AD /* AnyIPEndpoint+Wireguard.swift in Sources */,
58CE5E7C224146470008646E /* PacketTunnelProvider.swift in Sources */,
58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */,
@@ -912,6 +1002,7 @@
58B8743B22B788D200015324 /* PacketTunnelSettingsGenerator.swift in Sources */,
5860F1EB23AA4CF300CEA666 /* Logging.swift in Sources */,
5860F1C223A785C600CEA666 /* WireguardDevice.swift in Sources */,
+ 580EE21624B3231200F9D8A1 /* OperationBlockObserver.swift in Sources */,
58C6B35522BB87C4003C19AD /* WireguardPrivateKey.swift in Sources */,
58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */,
58AEEF6C2344A49D00C9BBD5 /* TunnelConfigurationManager.swift in Sources */,
@@ -920,18 +1011,25 @@
582650872384117900FA7A86 /* ReplaceNilWithError.swift in Sources */,
58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */,
58BFA5C022A7C8A900A6173D /* MullvadRpc.swift in Sources */,
+ 580EE21024B322E700F9D8A1 /* TransformOperation.swift in Sources */,
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */,
584E96BD240FD4DA00D3334F /* Location.swift in Sources */,
58FAEDF8245088E100CB0F5B /* Keychain.swift in Sources */,
58C6B36122C0EC82003C19AD /* AnyIPEndpoint+DNS64.swift in Sources */,
58F840B32464491D0044E708 /* ChainedError.swift in Sources */,
+ 580EE20A24B3224200F9D8A1 /* RetryOperation.swift in Sources */,
58C6B36722C106FC003C19AD /* WireguardCommand.swift in Sources */,
58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */,
+ 580EE22524B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */,
+ 580EE20D24B3225F00F9D8A1 /* DelayOperation.swift in Sources */,
588534BF246193D90018B744 /* AutomaticKeyRotationManager.swift in Sources */,
584B26FF237435A90073B10E /* RelaySelector+RelayCache.swift in Sources */,
58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */,
58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */,
+ 580EE21C24B3236900F9D8A1 /* InputOperation.swift in Sources */,
+ 580EE22224B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */,
5845F843236CBDAB00B2D93C /* PacketTunnelIpc.swift in Sources */,
+ 580EE22624B3245600F9D8A1 /* OperationObserver.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/ios/MullvadVPN/Operations/AnyOperationObserver.swift b/ios/MullvadVPN/Operations/AnyOperationObserver.swift
new file mode 100644
index 0000000000..72f98a92f9
--- /dev/null
+++ b/ios/MullvadVPN/Operations/AnyOperationObserver.swift
@@ -0,0 +1,15 @@
+//
+// AnyOperationObserver.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+class AnyOperationObserver<OperationType: OperationProtocol>: OperationBlockObserver<OperationType> {
+ init<T: OperationObserver>(_ observer: T) where T.OperationType == OperationType {
+ super.init(willFinish: observer.operationWillFinish, didFinish: observer.operationDidFinish)
+ }
+}
diff --git a/ios/MullvadVPN/Operations/AssociatedValue.swift b/ios/MullvadVPN/Operations/AssociatedValue.swift
new file mode 100644
index 0000000000..0f197086de
--- /dev/null
+++ b/ios/MullvadVPN/Operations/AssociatedValue.swift
@@ -0,0 +1,31 @@
+//
+// AssociatedValue.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/// A container type for storing associated values
+final class AssociatedValue<T>: NSObject {
+ let value: T
+ init(_ value: T) {
+ self.value = value
+ }
+
+ class func get(object: Any, key: UnsafeRawPointer) -> T? {
+ let container = objc_getAssociatedObject(object, key) as? Self
+ return container?.value
+ }
+
+ class func set(object: Any, key: UnsafeRawPointer, value: T?) {
+ objc_setAssociatedObject(
+ object,
+ key,
+ value.flatMap { AssociatedValue($0) },
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
+ )
+ }
+}
diff --git a/ios/MullvadVPN/Operations/AsyncBlockOperation.swift b/ios/MullvadVPN/Operations/AsyncBlockOperation.swift
new file mode 100644
index 0000000000..c8f4287b32
--- /dev/null
+++ b/ios/MullvadVPN/Operations/AsyncBlockOperation.swift
@@ -0,0 +1,26 @@
+//
+// AsyncBlockOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/// Asynchronous block operation
+class AsyncBlockOperation: AsyncOperation {
+ private let block: (@escaping () -> Void) -> Void
+
+ init(_ block: @escaping (@escaping () -> Void) -> Void) {
+ self.block = block
+ super.init()
+ }
+
+ override func main() {
+ self.block { [weak self] in
+ self?.finish()
+ }
+ }
+}
+
diff --git a/ios/MullvadVPN/Operations/AsyncOperation.swift b/ios/MullvadVPN/Operations/AsyncOperation.swift
new file mode 100644
index 0000000000..98a5f8aa39
--- /dev/null
+++ b/ios/MullvadVPN/Operations/AsyncOperation.swift
@@ -0,0 +1,142 @@
+//
+// AsyncOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 01/06/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/// A base implementation of an asynchronous operation
+class AsyncOperation: Operation, OperationProtocol {
+
+ /// A state lock used for manipulating the operation state flags in a thread safe fashion.
+ fileprivate let stateLock = NSRecursiveLock()
+
+ /// Operation state flags.
+ private var _isExecuting = false
+ private var _isFinished = false
+ private var _isCancelled = false
+
+ final override var isExecuting: Bool {
+ return stateLock.withCriticalBlock { _isExecuting }
+ }
+
+ final override var isFinished: Bool {
+ return stateLock.withCriticalBlock { _isFinished }
+ }
+
+ final override var isCancelled: Bool {
+ return stateLock.withCriticalBlock { _isCancelled }
+ }
+
+ final override var isAsynchronous: Bool {
+ return true
+ }
+
+ final override func start() {
+ stateLock.withCriticalBlock {
+ if self._isCancelled {
+ self.finish()
+ } else {
+ self.setExecuting(true)
+ self.main()
+ }
+ }
+ }
+
+ override func main() {
+ // Override in subclasses
+ }
+
+ /// Cancel operation
+ /// Subclasses should override `operationDidCancel` instead
+ final override func cancel() {
+ stateLock.withCriticalBlock {
+ if !self._isCancelled {
+ self.setCancelled(true)
+
+ if self._isExecuting {
+ self.operationDidCancel()
+ }
+ }
+ }
+ }
+
+ /// Override in subclasses to support task cancellation.
+ /// Subclasses should call `finish()` to complete the operation
+ func operationDidCancel() {
+ // no-op
+ }
+
+ final func finish() {
+ stateLock.withCriticalBlock {
+ if !self._isFinished {
+ self.observers.forEach { $0.operationWillFinish(self) }
+ }
+
+ if self._isExecuting {
+ self.setExecuting(false)
+ }
+
+ if !self._isFinished {
+ self.setFinished(true)
+ self.observers.forEach { $0.operationDidFinish(self) }
+ }
+ }
+ }
+
+ private func setExecuting(_ value: Bool) {
+ willChangeValue(for: \.isExecuting)
+ _isExecuting = value
+ didChangeValue(for: \.isExecuting)
+ }
+
+ private func setFinished(_ value: Bool) {
+ willChangeValue(for: \.isFinished)
+ _isFinished = value
+ didChangeValue(for: \.isFinished)
+ }
+
+ private func setCancelled(_ value: Bool) {
+ willChangeValue(for: \.isCancelled)
+ _isCancelled = value
+ didChangeValue(for: \.isCancelled)
+ }
+
+ // MARK: - Observation
+
+ /// The operation observers.
+ fileprivate var observers: [AnyOperationObserver<AsyncOperation>] = []
+
+ /// Add type-erased operation observer
+ fileprivate func addAnyObserver(_ observer: AnyOperationObserver<AsyncOperation>) {
+ stateLock.withCriticalBlock {
+ self.observers.append(observer)
+ }
+ }
+}
+
+/// This extension exists because Swift has some issues to infer the
+extension OperationProtocol where Self: AsyncOperation {
+ func addObserver<T: OperationObserver>(_ observer: T) where T.OperationType == Self {
+ let transform = TransformOperationObserver<AsyncOperation>(observer)
+ let wrapped = AnyOperationObserver(transform)
+ addAnyObserver(wrapped)
+ }
+}
+
+
+protocol OperationSubclassing {
+ /// Use this method in subclasses or extensions where you would like to synchronize
+ /// the class members access using the same lock used for guarding from race conditions
+ /// when managing operation state.
+ func synchronized<T>(_ body: () -> T) -> T
+}
+
+extension AsyncOperation: OperationSubclassing {
+ func synchronized<T>(_ body: () -> T) -> T {
+ return stateLock.withCriticalBlock(body)
+ }
+}
diff --git a/ios/MullvadVPN/Operations/DelayOperation.swift b/ios/MullvadVPN/Operations/DelayOperation.swift
new file mode 100644
index 0000000000..80719b573d
--- /dev/null
+++ b/ios/MullvadVPN/Operations/DelayOperation.swift
@@ -0,0 +1,48 @@
+//
+// DelayOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+enum DelayTimerType {
+ case deadline
+ case walltime
+}
+
+class DelayOperation: AsyncOperation {
+ private let delay: TimeInterval
+ private let timerType: DelayTimerType
+ private var timer: DispatchSourceTimer?
+
+ init(delay: TimeInterval, timerType: DelayTimerType) {
+ self.delay = delay
+ self.timerType = timerType
+ }
+
+ override func main() {
+ let timer = DispatchSource.makeTimerSource()
+ timer.setEventHandler { [weak self] in
+ self?.finish()
+ }
+
+ switch timerType {
+ case .deadline:
+ timer.schedule(deadline: DispatchTime.now() + delay)
+ case .walltime:
+ timer.schedule(wallDeadline: DispatchWallTime.now() + delay)
+ }
+
+ self.timer = timer
+ timer.activate()
+ }
+
+ override func operationDidCancel() {
+ timer?.cancel()
+ timer = nil
+ finish()
+ }
+}
diff --git a/ios/MullvadVPN/Operations/ExclusivityController.swift b/ios/MullvadVPN/Operations/ExclusivityController.swift
new file mode 100644
index 0000000000..9e18516869
--- /dev/null
+++ b/ios/MullvadVPN/Operations/ExclusivityController.swift
@@ -0,0 +1,69 @@
+//
+// ExclusivityController.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+class ExclusivityController<Category> where Category: Hashable {
+ private let operationQueue: OperationQueue
+ private let lock = NSRecursiveLock()
+
+ private var operations: [Category: [Operation]] = [:]
+ private var observers: [Operation: NSObjectProtocol] = [:]
+
+ init(operationQueue: OperationQueue) {
+ self.operationQueue = operationQueue
+ }
+
+ func addOperation(_ operation: Operation, categories: [Category]) {
+ addOperations([operation], categories: categories)
+ }
+
+ func addOperations(_ operations: [Operation], categories: [Category]) {
+ 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)
+ }
+ }
+ }
+
+ operationQueue.addOperations(operations, waitUntilFinished: false)
+ }
+ }
+
+ private func addDependencies(operation: Operation, category: Category) {
+ var exclusiveOperations = self.operations[category] ?? []
+
+ if let dependency = exclusiveOperations.last, !operation.dependencies.contains(dependency) {
+ operation.addDependency(dependency)
+ }
+
+ exclusiveOperations.append(operation)
+ self.operations[category] = exclusiveOperations
+ }
+
+ private func operationDidFinish(_ operation: Operation, categories: [Category]) {
+ lock.withCriticalBlock {
+ for category in categories {
+ var exclusiveOperations = self.operations[category] ?? []
+
+ exclusiveOperations.removeAll { (storedOperation) -> Bool in
+ return operation == storedOperation
+ }
+
+ self.operations[category] = exclusiveOperations
+ }
+ self.observers.removeValue(forKey: operation)
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Operations/InputOperation.swift b/ios/MullvadVPN/Operations/InputOperation.swift
new file mode 100644
index 0000000000..a1c9d6931d
--- /dev/null
+++ b/ios/MullvadVPN/Operations/InputOperation.swift
@@ -0,0 +1,103 @@
+//
+// InputOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol InputOperation: OperationProtocol {
+ associatedtype Input
+
+ /// When overriding `input` in Subclasses, make sure to call `operationDidSetInput`
+ var input: Input? { get set }
+
+ func operationDidSetInput(_ input: Input?)
+}
+
+private var kInputOperationAssociatedValue = 0
+extension InputOperation where Self: OperationSubclassing {
+ var input: Input? {
+ get {
+ return synchronized {
+ return AssociatedValue.get(object: self, key: &kInputOperationAssociatedValue)
+ }
+ }
+ set {
+ synchronized {
+ AssociatedValue.set(object: self, key: &kInputOperationAssociatedValue, value: newValue)
+
+ operationDidSetInput(newValue)
+ }
+ }
+ }
+
+ func operationDidSetInput(_ input: Input?) {
+ // Override in subclasses
+ }
+}
+
+extension InputOperation {
+
+ @discardableResult func inject<Dependency>(from dependency: Dependency, via block: @escaping (Dependency.Output) -> Input?) -> Self
+ where Dependency: OutputOperation
+ {
+ let observer = OperationBlockObserver<Dependency>(willFinish: { [weak self] (operation) in
+ guard let self = self else { return }
+
+ if let output = operation.output {
+ self.input = block(output)
+ }
+ })
+ dependency.addObserver(observer)
+ addDependency(dependency)
+
+ return self
+ }
+
+ @discardableResult func injectResult<Dependency>(from dependency: Dependency) -> Self
+ where Dependency: OutputOperation, Dependency.Output == Input?
+ {
+ return self.inject(from: dependency, via: { $0 })
+ }
+
+ /// Inject input from operation that outputs `Result<Input, Failure>`
+ @discardableResult func injectResult<Dependency, Failure>(from dependency: Dependency) -> Self
+ where Dependency: OutputOperation, Failure: Error, Dependency.Output == Result<Input, Failure>
+ {
+ return self.inject(from: dependency) { (output) -> Input? in
+ switch output {
+ case .success(let value):
+ return value
+ case .failure:
+ return nil
+ }
+ }
+ }
+
+ /// Inject input from operation that outputs `Result<Input, Never>`
+ @discardableResult func injectResult<Dependency>(from dependency: Dependency) -> Self
+ where Dependency: OutputOperation, Dependency.Output == Result<Input, Never>
+ {
+ return self.inject(from: dependency) { (output) -> Input? in
+ switch output {
+ case .success(let value):
+ return value
+ }
+ }
+ }
+
+ /// Inject input from operation that outputs `Result<Input?, Never>`
+ @discardableResult func injectResult<Dependency>(from dependency: Dependency) -> Self
+ where Dependency: OutputOperation, Dependency.Output == Result<Input?, Never>
+ {
+ return self.inject(from: dependency) { (output) -> Input? in
+ switch output {
+ case .success(let value):
+ return value
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Operations/OperationBlockObserver.swift b/ios/MullvadVPN/Operations/OperationBlockObserver.swift
new file mode 100644
index 0000000000..728e9a5a02
--- /dev/null
+++ b/ios/MullvadVPN/Operations/OperationBlockObserver.swift
@@ -0,0 +1,33 @@
+//
+// OperationBlockObserver.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+class OperationBlockObserver<OperationType: OperationProtocol>: OperationObserver {
+ private var willFinish: ((OperationType) -> Void)?
+ private var didFinish: ((OperationType) -> Void)?
+
+ init(willFinish: ((OperationType) -> Void)? = nil, didFinish: ((OperationType) -> Void)? = nil) {
+ self.willFinish = willFinish
+ self.didFinish = didFinish
+ }
+
+ func operationWillFinish(_ operation: OperationType) {
+ self.willFinish?(operation)
+ }
+
+ func operationDidFinish(_ operation: OperationType) {
+ self.didFinish?(operation)
+ }
+}
+
+extension OperationProtocol {
+ func addDidFinishBlockObserver(_ block: @escaping (Self) -> Void) {
+ addObserver(OperationBlockObserver(didFinish: block))
+ }
+}
diff --git a/ios/MullvadVPN/Operations/OperationObserver.swift b/ios/MullvadVPN/Operations/OperationObserver.swift
new file mode 100644
index 0000000000..295b993a10
--- /dev/null
+++ b/ios/MullvadVPN/Operations/OperationObserver.swift
@@ -0,0 +1,17 @@
+//
+// OperationObserver.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol OperationObserver {
+ associatedtype OperationType: OperationProtocol
+
+ func operationWillFinish(_ operation: OperationType)
+ func operationDidFinish(_ operation: OperationType)
+}
+
diff --git a/ios/MullvadVPN/Operations/OperationProtocol.swift b/ios/MullvadVPN/Operations/OperationProtocol.swift
new file mode 100644
index 0000000000..a41102c265
--- /dev/null
+++ b/ios/MullvadVPN/Operations/OperationProtocol.swift
@@ -0,0 +1,20 @@
+//
+// OperationProtocol.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol OperationProtocol: Operation {
+ /// Add operation observer
+ func addObserver<T: OperationObserver>(_ observer: T) where T.OperationType == Self
+
+ /// Finish operation
+ func finish()
+
+ /// Cancel operation
+ func cancel()
+}
diff --git a/ios/MullvadVPN/Operations/OutputOperation.swift b/ios/MullvadVPN/Operations/OutputOperation.swift
new file mode 100644
index 0000000000..533d5e5151
--- /dev/null
+++ b/ios/MullvadVPN/Operations/OutputOperation.swift
@@ -0,0 +1,50 @@
+//
+// OutputOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol OutputOperation: OperationProtocol {
+ associatedtype Output
+
+ var output: Output? { get set }
+
+ func finish(with output: Output)
+}
+
+extension OutputOperation {
+ func finish(with output: Output) {
+ self.output = output
+ self.finish()
+ }
+}
+
+private var kOutputOperationAssociatedValue = 0
+extension OutputOperation where Self: OperationSubclassing {
+ var output: Output? {
+ get {
+ return synchronized {
+ return AssociatedValue.get(object: self, key: &kOutputOperationAssociatedValue)
+ }
+ }
+ set {
+ synchronized {
+ AssociatedValue.set(object: self, key: &kOutputOperationAssociatedValue, value: newValue)
+ }
+ }
+ }
+}
+
+extension OperationProtocol where Self: OutputOperation {
+ func addDidFinishBlockObserver(_ block: @escaping (Self, Output) -> Void) {
+ addDidFinishBlockObserver { (operation) in
+ if let output = operation.output {
+ block(operation, output)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Operations/ResultOperation.swift b/ios/MullvadVPN/Operations/ResultOperation.swift
new file mode 100644
index 0000000000..7ce9c20358
--- /dev/null
+++ b/ios/MullvadVPN/Operations/ResultOperation.swift
@@ -0,0 +1,63 @@
+//
+// ResultOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+class ResultOperation<Success, Failure: Error>: AsyncOperation, OutputOperation {
+ typealias Output = Result<Success, Failure>
+
+ private enum Executor {
+ case callback((@escaping (Result<Success, Failure>) -> Void) -> Void)
+ case transform(() -> Result<Success, Failure>)
+ }
+
+ private let executor: Executor
+
+ private init(_ executor: Executor) {
+ self.executor = executor
+ }
+
+ convenience init(_ block: @escaping (@escaping (Output) -> Void) -> Void) {
+ self.init(.callback(block))
+ }
+
+ convenience init(_ block: @escaping () -> Output) {
+ self.init(.transform(block))
+ }
+
+ override func main() {
+ switch executor {
+ case .callback(let block):
+ block { [weak self] (result) in
+ self?.finish(with: result)
+ }
+
+ case .transform(let block):
+ self.finish(with: block())
+ }
+ }
+
+}
+
+extension ResultOperation where Failure == Never {
+ /// A convenience initializer for infallible `ResultOperation` that automatically wraps the
+ /// return value of the given closure into `Result<Success, Never>`
+ convenience init(_ block: @escaping () -> Success) {
+ self.init(.transform({ .success(block()) }))
+ }
+
+ /// A convenience initializer for infallible `ResultOperation` that automatically wraps the
+ /// value, passed to the given closure, into `Result<Success, Never>`
+ convenience init(_ block: @escaping (@escaping (Success) -> Void) -> Void) {
+ self.init(.callback({ (finish) in
+ block {
+ finish(.success($0))
+ }
+ }))
+ }
+}
diff --git a/ios/MullvadVPN/Operations/RetryOperation.swift b/ios/MullvadVPN/Operations/RetryOperation.swift
new file mode 100644
index 0000000000..1e73e81d79
--- /dev/null
+++ b/ios/MullvadVPN/Operations/RetryOperation.swift
@@ -0,0 +1,120 @@
+//
+// RetryOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+enum WaitStrategy {
+ case immediate
+ case constant(TimeInterval)
+
+ var iterator: AnyIterator<TimeInterval> {
+ switch self {
+ case .immediate:
+ return AnyIterator { .zero }
+ case .constant(let constant):
+ return AnyIterator { constant }
+ }
+ }
+}
+
+struct RetryStrategy {
+ var maxRetries: Int
+ var waitStrategy: WaitStrategy
+ var waitTimerType: DelayTimerType
+}
+
+class RetryOperation<OperationType, Success, Failure: Error>: AsyncOperation, OutputOperation
+ where OperationType: OutputOperation, OperationType.Output == Result<Success, Failure>
+{
+ typealias Output = OperationType.Output
+
+ private let operationQueue = OperationQueue()
+
+ private let producer: () -> OperationType
+ private let delayIterator: AnyIterator<TimeInterval>
+
+ private var retryCount: Int = 0
+ private let retryStrategy: RetryStrategy
+
+ private var childConfigurator: ((OperationType) -> Void)?
+
+ init(underlyingQueue: DispatchQueue? = nil, strategy: RetryStrategy, producer: @escaping () -> OperationType) {
+ operationQueue.underlyingQueue = underlyingQueue
+ delayIterator = strategy.waitStrategy.iterator
+ retryStrategy = strategy
+ self.producer = producer
+ }
+
+ override func main() {
+ retry()
+ }
+
+ override func operationDidCancel() {
+ operationQueue.cancelAllOperations()
+ }
+
+ private func retry(delay: TimeInterval? = nil) {
+ let child = producer()
+
+ child.addDidFinishBlockObserver { [weak self] (operation) in
+ guard let self = self else { return }
+
+ // Operation finished without output set?
+ guard let result = operation.output else {
+ self.finish()
+ return
+ }
+
+ self.synchronized {
+ guard case .failure(let error) = result,
+ let delay = self.delayIterator.next(),
+ self.shouldRetry(error: error) else {
+ self.finish(with: result)
+ return
+ }
+
+ self.retryCount += 1
+ self.retry(delay: delay)
+ }
+ }
+
+ synchronized {
+ childConfigurator?(child)
+ }
+
+ if let delay = delay {
+ let delayOperation = DelayOperation(delay: delay, timerType: retryStrategy.waitTimerType)
+
+ child.addDependency(delayOperation)
+ operationQueue.addOperation(delayOperation)
+ }
+
+ operationQueue.addOperation(child)
+ }
+
+ private func setChildConfigurator(_ body: @escaping (OperationType) -> Void) {
+ synchronized {
+ self.childConfigurator = body
+ }
+ }
+
+ private func shouldRetry(error: Failure) -> Bool {
+ return retryCount < retryStrategy.maxRetries && !self.isCancelled
+ }
+
+}
+
+extension RetryOperation: InputOperation where OperationType: InputOperation {
+ typealias Input = OperationType.Input
+
+ func operationDidSetInput(_ input: OperationType.Input?) {
+ setChildConfigurator { (child) in
+ child.input = input
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Operations/TransformOperation.swift b/ios/MullvadVPN/Operations/TransformOperation.swift
new file mode 100644
index 0000000000..ce2a1617ea
--- /dev/null
+++ b/ios/MullvadVPN/Operations/TransformOperation.swift
@@ -0,0 +1,50 @@
+//
+// TransformOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+class TransformOperation<Input, Output>: AsyncOperation, InputOperation, OutputOperation {
+ private enum Executor {
+ case callback((Input, @escaping (Output) -> Void) -> Void)
+ case transform((Input) -> Output)
+ }
+
+ private let executor: Executor
+
+ private init(input: Input? = nil, executor: Executor) {
+ self.executor = executor
+
+ super.init()
+ self.input = input
+ }
+
+ convenience init(input: Input? = nil, _ block: @escaping (Input, @escaping (Output) -> Void) -> Void) {
+ self.init(input: input, executor: .callback(block))
+ }
+
+ convenience init(input: Input? = nil, _ block: @escaping (Input) -> Output) {
+ self.init(input: input, executor: .transform(block))
+ }
+
+ override func main() {
+ guard let input = input else {
+ self.finish()
+ return
+ }
+
+ switch executor {
+ case .callback(let block):
+ block(input) { [weak self] (result) in
+ self?.finish(with: result)
+ }
+
+ case .transform(let block):
+ self.finish(with: block(input))
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Operations/TransformOperationObserver.swift b/ios/MullvadVPN/Operations/TransformOperationObserver.swift
new file mode 100644
index 0000000000..48fbe02dbb
--- /dev/null
+++ b/ios/MullvadVPN/Operations/TransformOperationObserver.swift
@@ -0,0 +1,39 @@
+//
+// TransformOperationObserver.swift
+// MullvadVPN
+//
+// Created by pronebird on 06/07/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/// A private type erasing observer that type casts the input operation type to the expected
+/// operation type before calling the wrapped observer
+class TransformOperationObserver<S: OperationProtocol>: OperationObserver {
+ private let willFinish: (S) -> Void
+ private let didFinish: (S) -> Void
+
+ init<T: OperationObserver>(_ observer: T) {
+ willFinish = Self.wrap(observer.operationWillFinish)
+ didFinish = Self.wrap(observer.operationDidFinish)
+ }
+
+ func operationWillFinish(_ operation: S) {
+ willFinish(operation)
+ }
+
+ func operationDidFinish(_ operation: S) {
+ didFinish(operation)
+ }
+
+ private class func wrap<U>(_ body: @escaping (U) -> Void) -> (S) -> Void {
+ return { (operation: S) in
+ if let transformed = operation as? U {
+ body(transformed)
+ } else {
+ fatalError("\(Self.self) failed to cast \(S.self) to \(U.self)")
+ }
+ }
+ }
+}