diff options
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 118 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/AnyOptional.swift | 22 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/AnyResult.swift | 23 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise+Optional.swift | 31 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise+ReceiveOn.swift | 33 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise+Result.swift | 136 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise.swift | 262 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/PromiseCompletion.swift | 51 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/PromiseObserver.swift | 27 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/PromiseTests.swift | 147 |
10 files changed, 830 insertions, 20 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index df43bbe265..d159f98b47 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -111,7 +111,12 @@ 585834F824D2BC1F00A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834F724D2BC1F00A8AF56 /* Logging */; }; 585834FC24D2BC9500A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834FB24D2BC9500A8AF56 /* Logging */; }; 585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585CA70E25F8C44600B47C62 /* UIMetrics.swift */; }; + 585DA8AF26B9492500B8C587 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; 585FE2F124E1365400439C50 /* LogStreamer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585FE2F024E1365400439C50 /* LogStreamer.swift */; }; + 5860392726D91B8400554C79 /* PromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE526D23C3D001CB97C /* PromiseTests.swift */; }; + 5860392926DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; + 5860392A26DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; + 5860392B26DCEE6300554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; }; 5868585524054096000B8131 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* AppButton.swift */; }; 5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */; }; @@ -179,6 +184,8 @@ 58B43C1925F77DB60002C8C3 /* ConnectMainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */; }; 58B67B482602079E008EF58E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */; }; + 58B93A1826C54D7E00A55733 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; }; + 58B93A2526C683B300A55733 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B993B02608A34500BA7811 /* LoginContentView.swift */; }; 58B9EB132488ED2100095626 /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB122488ED2100095626 /* AlertPresenter.swift */; }; 58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB142489139B00095626 /* DisplayChainedError.swift */; }; @@ -192,6 +199,7 @@ 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; }; 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; + 58C3478B26C1094F0060838B /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */; }; 58C3B06724EA768100C0348E /* LogStreamerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3B06624EA768100C0348E /* LogStreamerViewController.swift */; }; 58C3B06924EAA25000C0348E /* StringStreamIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3B06824EAA25000C0348E /* StringStreamIterator.swift */; }; @@ -216,6 +224,24 @@ 58D0C7A223F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */; }; 58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */; }; + 58E1336926D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; + 58E1336A26D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; + 58E1336B26D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; + 58E1336D26D2BE7500CC316B /* AnyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336C26D2BE7500CC316B /* AnyResult.swift */; }; + 58E1336E26D2BE7500CC316B /* AnyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336C26D2BE7500CC316B /* AnyResult.swift */; }; + 58E1336F26D2BE7500CC316B /* AnyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336C26D2BE7500CC316B /* AnyResult.swift */; }; + 58E1337126D2BE9C00CC316B /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; + 58E1337226D2BE9C00CC316B /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; + 58E1337326D2BE9C00CC316B /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; + 58E1337526D2BEC400CC316B /* Promise+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337426D2BEC400CC316B /* Promise+Optional.swift */; }; + 58E1337626D2BEC400CC316B /* Promise+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337426D2BEC400CC316B /* Promise+Optional.swift */; }; + 58E1337726D2BEC400CC316B /* Promise+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337426D2BEC400CC316B /* Promise+Optional.swift */; }; + 58E1337926D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */; }; + 58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */; }; + 58E1337B26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */; }; + 58E1338126D2BF5C00CC316B /* Promise+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1338026D2BF5C00CC316B /* Promise+Result.swift */; }; + 58E1338226D2BF5C00CC316B /* Promise+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1338026D2BF5C00CC316B /* Promise+Result.swift */; }; + 58E1338326D2BF5C00CC316B /* Promise+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1338026D2BF5C00CC316B /* Promise+Result.swift */; }; 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */; }; 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */; }; 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF581025D69DB400AEBA94 /* StatusImageView.swift */; }; @@ -234,12 +260,12 @@ 58F558F92696EB1C00F630D0 /* StoreKitErrors.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558F32696EB1C00F630D0 /* StoreKitErrors.strings */; }; 58F558FA2696EB1C00F630D0 /* TunnelManager.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558F52696EB1C00F630D0 /* TunnelManager.strings */; }; 58F558FB2696EB1C00F630D0 /* AppStorePaymentManager.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558F72696EB1C00F630D0 /* AppStorePaymentManager.strings */; }; - 58F5590E2697002100F630D0 /* Main.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F559052697002000F630D0 /* Main.strings */; }; - 58F5590F2697002100F630D0 /* ConnectionPanel.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F559072697002100F630D0 /* ConnectionPanel.strings */; }; 58F558FE2696F09100F630D0 /* KeyboardNavigation.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558FC2696F09100F630D0 /* KeyboardNavigation.strings */; }; 58F5590B2697002100F630D0 /* CustomDateComponentsFormatting.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F558FF2697002000F630D0 /* CustomDateComponentsFormatting.strings */; }; 58F5590C2697002100F630D0 /* AppDelegate.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F559012697002000F630D0 /* AppDelegate.strings */; }; 58F5590D2697002100F630D0 /* AccountInput.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F559032697002000F630D0 /* AccountInput.strings */; }; + 58F5590E2697002100F630D0 /* Main.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F559052697002000F630D0 /* Main.strings */; }; + 58F5590F2697002100F630D0 /* ConnectionPanel.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F559072697002100F630D0 /* ConnectionPanel.strings */; }; 58F559102697002100F630D0 /* HeaderBar.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F559092697002100F630D0 /* HeaderBar.strings */; }; 58F61F4F2692F21C00DCFC2B /* WireguardKeys.strings in Resources */ = {isa = PBXBuildFile; fileRef = 58F61F4D2692F21C00DCFC2B /* WireguardKeys.strings */; }; 58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */; }; @@ -367,7 +393,9 @@ 5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationHeaderView.swift; sourceTree = "<group>"; }; 5857F24624C882D700CF6F47 /* SelectLocationNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNavigationController.swift; sourceTree = "<group>"; }; 585CA70E25F8C44600B47C62 /* UIMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMetrics.swift; sourceTree = "<group>"; }; + 585DA8AE26B9492500B8C587 /* Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = "<group>"; }; 585FE2F024E1365400439C50 /* LogStreamer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStreamer.swift; sourceTree = "<group>"; }; + 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseCompletion.swift; sourceTree = "<group>"; }; 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; }; 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MullvadVPN.entitlements; sourceTree = "<group>"; }; 5868585424054096000B8131 /* AppButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = "<group>"; }; @@ -406,6 +434,7 @@ 589AB4F6227B64450039131E /* BasicTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicTableViewCell.swift; sourceTree = "<group>"; }; 58A1AA8623F43901009F7EA6 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = "<group>"; }; 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPanelView.swift; sourceTree = "<group>"; }; + 58A94AE526D23C3D001CB97C /* PromiseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseTests.swift; sourceTree = "<group>"; }; 58A99ED2240014A0006599E9 /* ConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentViewController.swift; sourceTree = "<group>"; }; 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = "<group>"; }; 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSwitchCell.swift; sourceTree = "<group>"; }; @@ -450,6 +479,12 @@ 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>"; }; + 58E1336826D2BE3700CC316B /* PromiseObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseObserver.swift; sourceTree = "<group>"; }; + 58E1336C26D2BE7500CC316B /* AnyResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyResult.swift; sourceTree = "<group>"; }; + 58E1337026D2BE9C00CC316B /* AnyOptional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyOptional.swift; sourceTree = "<group>"; }; + 58E1337426D2BEC400CC316B /* Promise+Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Optional.swift"; sourceTree = "<group>"; }; + 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+ReceiveOn.swift"; sourceTree = "<group>"; }; + 58E1338026D2BF5C00CC316B /* Promise+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Result.swift"; sourceTree = "<group>"; }; 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigationController.swift; sourceTree = "<group>"; }; 58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; }; 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = "<group>"; }; @@ -468,12 +503,12 @@ 58F558F42696EB1C00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/StoreKitErrors.strings; sourceTree = "<group>"; }; 58F558F62696EB1C00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TunnelManager.strings; sourceTree = "<group>"; }; 58F558F82696EB1C00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppStorePaymentManager.strings; sourceTree = "<group>"; }; - 58F559062697002000F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = "<group>"; }; - 58F559082697002100F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ConnectionPanel.strings; sourceTree = "<group>"; }; 58F558FD2696F09100F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/KeyboardNavigation.strings; sourceTree = "<group>"; }; 58F559002697002000F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/CustomDateComponentsFormatting.strings; sourceTree = "<group>"; }; 58F559022697002000F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppDelegate.strings; sourceTree = "<group>"; }; 58F559042697002000F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AccountInput.strings; sourceTree = "<group>"; }; + 58F559062697002000F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = "<group>"; }; + 58F559082697002100F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ConnectionPanel.strings; sourceTree = "<group>"; }; 58F5590A2697002100F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/HeaderBar.strings; sourceTree = "<group>"; }; 58F61F4E2692F21C00DCFC2B /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/WireguardKeys.strings; sourceTree = "<group>"; }; 58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardKeysContentView.swift; sourceTree = "<group>"; }; @@ -626,6 +661,7 @@ 58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = { isa = PBXGroup; children = ( + 58A94AE526D23C3D001CB97C /* PromiseTests.swift */, 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */, 58B0A2A4238EE67E00BC001D /* Info.plist */, 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, @@ -736,6 +772,7 @@ 587B75422669034500DEF7E9 /* Notifications */, 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */, 58CC40EE24A601900019D96E /* ObserverList.swift */, + 58E1338C26D2BFB500CC316B /* Promise */, 580EE1FF24B3218800F9D8A1 /* Operations */, 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */, 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */, @@ -802,6 +839,21 @@ path = MullvadVPNScreenshots; sourceTree = "<group>"; }; + 58E1338C26D2BFB500CC316B /* Promise */ = { + isa = PBXGroup; + children = ( + 585DA8AE26B9492500B8C587 /* Promise.swift */, + 58E1336826D2BE3700CC316B /* PromiseObserver.swift */, + 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */, + 58E1336C26D2BE7500CC316B /* AnyResult.swift */, + 58E1337026D2BE9C00CC316B /* AnyOptional.swift */, + 58E1337426D2BEC400CC316B /* Promise+Optional.swift */, + 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */, + 58E1338026D2BF5C00CC316B /* Promise+Result.swift */, + ); + path = Promise; + sourceTree = "<group>"; + }; 58ECD29023F178FD004298B6 /* Configurations */ = { isa = PBXGroup; children = ( @@ -1062,12 +1114,14 @@ buildActionMask = 2147483647; files = ( 5896AE81246ACE81005B36CB /* KeychainMatchLimit.swift in Sources */, + 58E1336F26D2BE7500CC316B /* AnyResult.swift in Sources */, 5896AE80246ACE79005B36CB /* KeychainClass.swift in Sources */, 582AE3132440CA2700E6733A /* AccountTokenInput.swift in Sources */, 58CAF4EF26025954007C5886 /* SimulatorTunnelProvider.swift in Sources */, 5857F23724C8446400CF6F47 /* AssociatedValue.swift in Sources */, 5857F23B24C8448600CF6F47 /* OperationProtocol.swift in Sources */, 58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */, + 58E1337726D2BEC400CC316B /* Promise+Optional.swift in Sources */, 5857F23924C8446A00CF6F47 /* AnyOperationObserver.swift in Sources */, 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */, 5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */, @@ -1078,11 +1132,16 @@ 5857F23F24C844AD00CF6F47 /* Locking.swift in Sources */, 5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */, 5857F23624C8445300CF6F47 /* OutputOperation.swift in Sources */, + 58E1338326D2BF5C00CC316B /* Promise+Result.swift in Sources */, 58B0A2AC238EE6D500BC001D /* IPAddress+Codable.swift in Sources */, 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */, + 5860392B26DCEE6300554C79 /* PromiseCompletion.swift in Sources */, 58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */, + 5860392726D91B8400554C79 /* PromiseTests.swift in Sources */, + 58E1337326D2BE9C00CC316B /* AnyOptional.swift in Sources */, 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */, 5857F23D24C8449A00CF6F47 /* TransformOperationObserver.swift in Sources */, + 58C3478B26C1094F0060838B /* Promise.swift in Sources */, 5857F23524C8444E00CF6F47 /* InputOperation.swift in Sources */, 5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */, 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */, @@ -1092,8 +1151,10 @@ 58CAF4EA26025927007C5886 /* PacketTunnelIpc.swift in Sources */, 5857F23E24C844A000CF6F47 /* OperationBlockObserver.swift in Sources */, 5857F23024C843ED00CF6F47 /* ChainedError.swift in Sources */, + 58E1337B26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */, 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */, 5896AE7F246ACE76005B36CB /* Keychain.swift in Sources */, + 58E1336B26D2BE3700CC316B /* PromiseObserver.swift in Sources */, 5857F23C24C8449500CF6F47 /* OperationObserver.swift in Sources */, 58871D1825D5359B002297FA /* MullvadRest.swift in Sources */, 58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */, @@ -1113,7 +1174,9 @@ 5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, 5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */, 580EE21224B322FC00F9D8A1 /* ResultOperation.swift in Sources */, + 58E1337126D2BE9C00CC316B /* AnyOptional.swift in Sources */, 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */, + 58E1336D26D2BE7500CC316B /* AnyResult.swift in Sources */, 587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */, 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */, 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */, @@ -1127,6 +1190,7 @@ 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */, 584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */, 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */, + 5860392926DCE7AB00554C79 /* PromiseCompletion.swift in Sources */, 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */, 5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */, 587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */, @@ -1137,6 +1201,7 @@ 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */, 58CB0EE024B86751001EF0D8 /* MullvadRest.swift in Sources */, 580EE20924B3224200F9D8A1 /* RetryOperation.swift in Sources */, + 58E1337526D2BEC400CC316B /* Promise+Optional.swift in Sources */, 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */, 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */, 58FAEDF7245088E100CB0F5B /* Keychain.swift in Sources */, @@ -1144,6 +1209,7 @@ 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */, 58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */, 5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */, + 58E1336926D2BE3700CC316B /* PromiseObserver.swift in Sources */, 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */, 580EE20424B321EC00F9D8A1 /* OperationObserver.swift in Sources */, 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */, @@ -1183,6 +1249,7 @@ 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */, 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */, 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */, + 58E1337926D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */, 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, 580EE21B24B3236900F9D8A1 /* InputOperation.swift in Sources */, 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, @@ -1206,6 +1273,7 @@ 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */, 589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */, 58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */, + 585DA8AF26B9492500B8C587 /* Promise.swift in Sources */, 587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */, 5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */, 58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */, @@ -1230,6 +1298,7 @@ 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */, 58C4CB0124EBE5A700A22D49 /* LogEntryParser.swift in Sources */, 58F840B22464491D0044E708 /* ChainedError.swift in Sources */, + 58E1338126D2BF5C00CC316B /* Promise+Result.swift in Sources */, 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */, 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */, 580EE20C24B3225F00F9D8A1 /* DelayOperation.swift in Sources */, @@ -1246,12 +1315,16 @@ 580EE21F24B3237F00F9D8A1 /* OutputOperation.swift in Sources */, 5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */, 580EE20224B321DB00F9D8A1 /* OperationProtocol.swift in Sources */, + 58B93A1826C54D7E00A55733 /* Locking.swift in Sources */, 58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */, + 5860392A26DCE7AB00554C79 /* PromiseCompletion.swift in Sources */, 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */, 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, 5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */, 580EE20724B3222400F9D8A1 /* ExclusivityController.swift in Sources */, 58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */, + 58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */, + 58B93A2526C683B300A55733 /* Promise.swift in Sources */, 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */, 58F3C0962492617E003E76BE /* AsyncOperation.swift in Sources */, 580EE22924B3289300F9D8A1 /* AssociatedValue.swift in Sources */, @@ -1266,10 +1339,14 @@ 58BA692F23E99F5B009DC256 /* Locking.swift in Sources */, 580EE21624B3231200F9D8A1 /* OperationBlockObserver.swift in Sources */, 58CC40F024A602780019D96E /* ObserverList.swift in Sources */, + 58E1337226D2BE9C00CC316B /* AnyOptional.swift in Sources */, + 58E1338226D2BF5C00CC316B /* Promise+Result.swift in Sources */, 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */, 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */, 58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */, + 58E1336E26D2BE7500CC316B /* AnyResult.swift in Sources */, 581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */, + 58E1336A26D2BE3700CC316B /* PromiseObserver.swift in Sources */, 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, 583BC70824FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */, @@ -1286,6 +1363,7 @@ 580EE22524B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */, 580EE20D24B3225F00F9D8A1 /* DelayOperation.swift in Sources */, 588534BF246193D90018B744 /* AutomaticKeyRotationManager.swift in Sources */, + 58E1337626D2BEC400CC316B /* Promise+Optional.swift in Sources */, 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */, 581503A024D6F01E00C9C50E /* LogRotation.swift in Sources */, 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */, @@ -1443,22 +1521,6 @@ name = AppStorePaymentManager.strings; sourceTree = "<group>"; }; - 58F559052697002000F630D0 /* Main.strings */ = { - isa = PBXVariantGroup; - children = ( - 58F559062697002000F630D0 /* en */, - ); - name = Main.strings; - sourceTree = "<group>"; - }; - 58F559072697002100F630D0 /* ConnectionPanel.strings */ = { - isa = PBXVariantGroup; - children = ( - 58F559082697002100F630D0 /* en */, - ); - name = ConnectionPanel.strings; - sourceTree = "<group>"; - }; 58F558FC2696F09100F630D0 /* KeyboardNavigation.strings */ = { isa = PBXVariantGroup; children = ( @@ -1491,6 +1553,22 @@ name = AccountInput.strings; sourceTree = "<group>"; }; + 58F559052697002000F630D0 /* Main.strings */ = { + isa = PBXVariantGroup; + children = ( + 58F559062697002000F630D0 /* en */, + ); + name = Main.strings; + sourceTree = "<group>"; + }; + 58F559072697002100F630D0 /* ConnectionPanel.strings */ = { + isa = PBXVariantGroup; + children = ( + 58F559082697002100F630D0 /* en */, + ); + name = ConnectionPanel.strings; + sourceTree = "<group>"; + }; 58F559092697002100F630D0 /* HeaderBar.strings */ = { isa = PBXVariantGroup; children = ( diff --git a/ios/MullvadVPN/Promise/AnyOptional.swift b/ios/MullvadVPN/Promise/AnyOptional.swift new file mode 100644 index 0000000000..95382ac864 --- /dev/null +++ b/ios/MullvadVPN/Promise/AnyOptional.swift @@ -0,0 +1,22 @@ +// +// AnyOptional.swift +// AnyOptional +// +// Created by pronebird on 22/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Protocol uniting all Optional types. +protocol AnyOptional { + associatedtype Wrapped + + func asConcreteType() -> Optional<Wrapped> +} + +extension Optional: AnyOptional { + func asConcreteType() -> Optional<Wrapped> { + return self + } +} diff --git a/ios/MullvadVPN/Promise/AnyResult.swift b/ios/MullvadVPN/Promise/AnyResult.swift new file mode 100644 index 0000000000..24e8e1fd11 --- /dev/null +++ b/ios/MullvadVPN/Promise/AnyResult.swift @@ -0,0 +1,23 @@ +// +// AnyResult.swift +// AnyResult +// +// Created by pronebird on 22/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Protocol uniting all Result types. +protocol AnyResult { + associatedtype Success + associatedtype Failure: Error + + func asConcreteType() -> Result<Success, Failure> +} + +extension Result: AnyResult { + func asConcreteType() -> Result<Success, Failure> { + return self + } +} diff --git a/ios/MullvadVPN/Promise/Promise+Optional.swift b/ios/MullvadVPN/Promise/Promise+Optional.swift new file mode 100644 index 0000000000..0fc38a1ae6 --- /dev/null +++ b/ios/MullvadVPN/Promise/Promise+Optional.swift @@ -0,0 +1,31 @@ +// +// Promise+Optional.swift +// Promise+Optional +// +// Created by pronebird on 22/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension Optional { + func asPromise() -> Promise<Self> { + return .resolved(self) + } +} + +extension Promise where Value: AnyOptional { + /// Map the value when present. Returns `defaultValue` otherwise. + func map<NewValue>(defaultValue: NewValue, transform: @escaping (Value.Wrapped) -> NewValue) -> Promise<NewValue> { + return then { value -> NewValue in + return value.asConcreteType().map(transform) ?? defaultValue + } + } + + /// Map the value when present, producing new promise to compute the new value. Returns `defaultValue` otherwise. + func mapThen<NewValue>(defaultValue: NewValue, producePromise: @escaping (Value.Wrapped) -> Promise<NewValue>) -> Promise<NewValue> { + return then { value in + return value.asConcreteType().map(producePromise) ?? .resolved(defaultValue) + } + } +} diff --git a/ios/MullvadVPN/Promise/Promise+ReceiveOn.swift b/ios/MullvadVPN/Promise/Promise+ReceiveOn.swift new file mode 100644 index 0000000000..a7ce8f039b --- /dev/null +++ b/ios/MullvadVPN/Promise/Promise+ReceiveOn.swift @@ -0,0 +1,33 @@ +// +// Promise+ReceiveOn.swift +// Promise+ReceiveOn +// +// Created by pronebird on 22/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension Promise { + /// Dispatch the upstream value on another queue. + func receive(on queue: DispatchQueue) -> Promise<Value> { + return Promise<Value> { resolver in + _ = self.observe { completion in + queue.async { + resolver.resolve(completion: completion, queue: queue) + } + } + } + } + + /// Dispatch the upstream value on another queue after delay. + func receive(on queue: DispatchQueue, after deadline: DispatchTime) -> Promise<Value> { + return Promise<Value> { resolver in + _ = self.observe { completion in + queue.asyncAfter(deadline: deadline) { + resolver.resolve(completion: completion, queue: queue) + } + } + } + } +} diff --git a/ios/MullvadVPN/Promise/Promise+Result.swift b/ios/MullvadVPN/Promise/Promise+Result.swift new file mode 100644 index 0000000000..94dd511227 --- /dev/null +++ b/ios/MullvadVPN/Promise/Promise+Result.swift @@ -0,0 +1,136 @@ +// +// Promise+Result.swift +// Promise+Result +// +// Created by pronebird on 22/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +typealias _Promise = Promise + +extension Result { + typealias Promise = _Promise<Result<Success, Failure>> +} + +extension Promise where Value: AnyResult { + typealias Success = Value.Success + typealias Failure = Value.Failure + + static func failure(_ error: Failure) -> Result<Success, Failure>.Promise { + return Result<Success, Failure>.Promise(value: .failure(error)) + } + + static func success(_ value: Success) -> Result<Success, Failure>.Promise { + return Result<Success, Failure>.Promise(value: .success(value)) + } + + /// Replace value in Result. Passes failure result downstream. + func setOutput<NewSuccess>(_ newValue: NewSuccess) -> Result<NewSuccess, Failure>.Promise { + return map { _ in + return newValue + } + } + /// Returns a Promise containing resolved value or nil. + func success() -> Promise<Success?> { + return then { result -> Success? in + switch result.asConcreteType() { + case .success(let value): + return value + case .failure: + return nil + } + } + } + + /// Map value. Passes failure result downstream. + func map<NewSuccess>(_ transform: @escaping (Success) -> NewSuccess) -> Result<NewSuccess, Failure>.Promise { + return then { result in + return result.asConcreteType().map(transform) + } + } + + /// Perform actiion on success. + func onSuccess(_ onResolve: @escaping (Success) -> Void) -> Self { + return observe { completion in + if case .success(let value) = completion.unwrappedValue?.asConcreteType() { + onResolve(value) + } + } + } + + /// Perform action on failure. + func onFailure(_ onResolve: @escaping (Failure) -> Void) -> Self { + return observe { completion in + if case .failure(let error) = completion.unwrappedValue?.asConcreteType() { + onResolve(error) + } + } + } + + /// Map value producing Promise. Passes failure result downstream. + func mapThen<NewSuccess>(_ transform: @escaping (Success) -> Result<NewSuccess, Failure>.Promise) -> Result<NewSuccess, Failure>.Promise { + return then { result in + switch result.asConcreteType() { + case .success(let value): + return transform(value) + case .failure(let error): + return .failure(error) + } + } + } + + /// Map failure. Passes successful result downstream. + func mapError<NewFailure>(_ transform: @escaping (Failure) -> NewFailure) -> Result<Success, NewFailure>.Promise { + return then { result in + return result.asConcreteType().mapError(transform) + } + } + + /// Map value to Result. Passes failure result downstream. + func flatMap<NewSuccess>(_ transform: @escaping (Success) -> Result<NewSuccess, Failure>) -> Result<NewSuccess, Failure>.Promise { + return then { result in + return result.asConcreteType().flatMap(transform) + } + } + + /// Map failure to Result. Passes successful result downstream. + func flatMapError<NewFailure>(_ transform: @escaping (Failure) -> Result<Success, NewFailure>) -> Result<Success, NewFailure>.Promise { + return then { result in + result.asConcreteType().flatMapError(transform) + } + } +} + +extension Promise where Value: AnyResult { + func tryAwait() throws -> PromiseCompletion<Value.Success> { + return try self.await().map { result in + return try result.asConcreteType().get() + } + } +} + +extension Result { + func asPromise() -> Result<Success, Failure>.Promise { + return .resolved(self) + } + + var error: Failure? { + switch self { + case .success: + return nil + case .failure(let error): + return error + } + } + + var value: Success? { + switch self { + case .success(let value): + return value + case .failure: + return nil + } + } +} diff --git a/ios/MullvadVPN/Promise/Promise.swift b/ios/MullvadVPN/Promise/Promise.swift new file mode 100644 index 0000000000..1e8d75be4f --- /dev/null +++ b/ios/MullvadVPN/Promise/Promise.swift @@ -0,0 +1,262 @@ +// +// Promise.swift +// Promise +// +// Created by pronebird on 03/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +private enum PromiseState<Value> { + case pending((PromiseResolver<Value>) -> Void, DispatchQueue?) + case executing + case resolved(Value, DispatchQueue?) + case cancelled +} + +/// Class describing a block of asynchronous computation that can either resolve or be cancelled. +final class Promise<Value> { + private var state: PromiseState<Value> + private var observers: [AnyPromiseObserver<Value>] = [] + private let lock = NSRecursiveLock() + + /// Returns Promise resolved with the given value. + class func resolved(_ value: Value) -> Self { + return Self.init(value: value) + } + + /// Initialize Promise with the execution block. + init(body: @escaping (PromiseResolver<Value>) -> Void) { + state = .pending(body, nil) + } + + /// Initialize resolved Promise with the given value. + init(value: Value) { + state = .resolved(value, nil) + } + + deinit { + switch state { + case .resolved, .cancelled: + break + case .pending, .executing: + preconditionFailure("\(Self.self) is deallocated in \(state) state without being resolved or cancelled.") + } + } + + /// Observe the result of Promise. + /// This method starts the promise execution if it hasn't started yet. + @discardableResult + func observe(_ receiveCompletion: @escaping (PromiseCompletion<Value>) -> Void) -> Self { + return lock.withCriticalBlock { + switch state { + case .resolved(let value, let queue): + let completion = PromiseCompletion<Value>.finished(value) + queue?.async { receiveCompletion(completion) } ?? receiveCompletion(completion) + + case .cancelled: + receiveCompletion(.cancelled) + + case .pending: + observers.append(AnyPromiseObserver<Value>(receiveCompletion)) + execute() + + case .executing: + observers.append(AnyPromiseObserver<Value>(receiveCompletion)) + } + return self + } + } + + /// Cancel Promise. + /// When Promise is cancelled, all downstream Promises pending execution are also cancelled. + func cancel() { + lock.withCriticalBlock { + switch state { + case .pending, .executing: + state = .cancelled + observers.forEach { observer in + observer.receiveCompletion(.cancelled) + } + observers.removeAll() + + case .cancelled, .resolved: + break + } + } + } + + /// Trasform the value by producing a promise. + func then<NewValue>(_ onResolve: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { + return Promise<NewValue> { resolver in + _ = self.observe { completion in + switch completion { + case .finished(let value): + _ = onResolve(value).observe { completion in + resolver.resolve(completion: completion) + } + case .cancelled: + resolver.resolve(completion: .cancelled) + } + } + } + } + + /// Transform the value. + func then<NewValue>(_ onResolve: @escaping (Value) -> NewValue) -> Promise<NewValue> { + return Promise<NewValue> { resolver in + _ = self.observe { completion in + resolver.resolve(completion: completion.map(onResolve)) + } + } + } + + /// Assign the cancellation token into the given variable. + /// Releasing the cancellation token cancels the given Promise and all downstream Promises. + func storeCancellationToken(in token: inout PromiseCancellationToken?) -> Self { + token = PromiseCancellationToken { [weak self] in + self?.cancel() + } + return self + } + + /// Set the queue on which to execute the promise's body block. + func schedule(on queue: DispatchQueue) -> Self { + return lock.withCriticalBlock { + switch state { + case .pending(let block, _): + state = .pending(block, queue) + case .cancelled, .executing, .resolved: + break + } + return self + } + } + + /// Block the given queue until the promise finished executing. + func block(on dispatchQueue: DispatchQueue) -> Promise<Value> { + return Promise { resolver in + dispatchQueue.async { + let completion = self.await() + + resolver.resolve(completion: completion) + } + } + } + + /// Block current queue until the promise finished executing. + func await() -> PromiseCompletion<Value> { + let condition = NSCondition() + condition.lock() + defer { condition.unlock() } + + var returnValue: PromiseCompletion<Value>! + _ = observe { completion in + returnValue = completion + condition.signal() + } + + condition.wait() + return returnValue + } + + /// Execute the promise's body if still pending execution. + private func execute() { + lock.withCriticalBlock { + guard case .pending(let block, let queue) = state else { return } + + state = .executing + + let resolver = PromiseResolver(promise: self) + + queue?.async { block(resolver) } ?? block(resolver) + } + } + + /// Resolve Promise with the given value. + /// + /// Provide the optional `queue` parameter which will be used to dispatch the resolved value to observers added + /// after the promise was already resolved. When providing a `queue`, the call to `resolve()` must happen on + /// the same queue. + fileprivate func resolve(value: Value, queue: DispatchQueue?) { + lock.withCriticalBlock { + switch state { + case .pending, .executing: + // Oblige caller to resolve the value on the same queue. + queue.map { dispatchPrecondition(condition: .onQueue($0)) } + + state = .resolved(value, queue) + + observers.forEach { observer in + observer.receiveCompletion(.finished(value)) + } + observers.removeAll() + + case .cancelled, .resolved: + break + } + + } + } + +} + +final class PromiseCancellationToken { + private let handler: () -> Void + fileprivate init(_ handler: @escaping () -> Void) { + self.handler = handler + } + + deinit { + handler() + } +} + +struct PromiseResolver<Value> { + private let promise: Promise<Value> + + /// Private initializer. + fileprivate init(promise: Promise<Value>) { + self.promise = promise + } + + /// Resolve the promise with `PromiseCompletion`. + func resolve(completion: PromiseCompletion<Value>) { + resolve(completion: completion, queue: nil) + } + + /// Resolve the promise with `PromiseCompletion` and ptiona queue on which to dispatch the value too observers added + /// after the promise was already resolved. + func resolve(completion: PromiseCompletion<Value>, queue: DispatchQueue?) { + switch completion { + case .finished(let value): + resolve(value: value, queue: queue) + case .cancelled: + promise.cancel() + } + } + + /// Resolve Promise with the given value. + func resolve(value: Value) { + resolve(value: value, queue: nil) + } + + /// Resolve the promise with the given value and optional queue on which to dispatch the value to observers added + /// after the promise was already resolved. + fileprivate func resolve(value: Value, queue: DispatchQueue?) { + promise.resolve(value: value, queue: queue) + } + + /// Set cancellation handler. + func setCancelHandler(_ cancellation: @escaping () -> Void) { + _ = promise.observe { completion in + switch completion { + case .finished: + break + case .cancelled: + cancellation() + } + } + } +} diff --git a/ios/MullvadVPN/Promise/PromiseCompletion.swift b/ios/MullvadVPN/Promise/PromiseCompletion.swift new file mode 100644 index 0000000000..b424e29518 --- /dev/null +++ b/ios/MullvadVPN/Promise/PromiseCompletion.swift @@ -0,0 +1,51 @@ +// +// PromiseCompletion.swift +// PromiseCompletion +// +// Created by pronebird on 30/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Promise result type. +enum PromiseCompletion<Value> { + /// Promise is finished with value. + case finished(Value) + + /// Promise is cancelled. + case cancelled + + /// Return the contained value, otherwise `nil`. + var unwrappedValue: Value? { + switch self { + case .finished(let value): + return value + case .cancelled: + return nil + } + } + + /// Map the contained value, producing new `PromiseCompletion` type. + func map<NewValue>(_ transform: (Value) throws -> NewValue) rethrows -> PromiseCompletion<NewValue> { + switch self { + case .finished(let value): + return .finished(try transform(value)) + case .cancelled: + return .cancelled + } + } +} + +extension PromiseCompletion: Equatable where Value: Equatable { + static func == (lhs: PromiseCompletion<Value>, rhs: PromiseCompletion<Value>) -> Bool { + switch (lhs, rhs) { + case (.finished(let lhsValue), .finished(let rhsValue)): + return lhsValue == rhsValue + case (.cancelled, .cancelled): + return true + case (.finished, .cancelled), (.cancelled, .finished): + return false + } + } +} diff --git a/ios/MullvadVPN/Promise/PromiseObserver.swift b/ios/MullvadVPN/Promise/PromiseObserver.swift new file mode 100644 index 0000000000..89e2059249 --- /dev/null +++ b/ios/MullvadVPN/Promise/PromiseObserver.swift @@ -0,0 +1,27 @@ +// +// PromiseObserver.swift +// PromiseObserver +// +// Created by pronebird on 22/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol PromiseObserver { + associatedtype Value + + func receiveCompletion(_ completion: PromiseCompletion<Value>) +} + +final class AnyPromiseObserver<Value>: PromiseObserver { + private let onReceiveCompletion: (PromiseCompletion<Value>) -> Void + + init(_ receiveCompletionHandler: @escaping (PromiseCompletion<Value>) -> Void) { + onReceiveCompletion = receiveCompletionHandler + } + + func receiveCompletion(_ completion: PromiseCompletion<Value>) { + onReceiveCompletion(completion) + } +} diff --git a/ios/MullvadVPNTests/PromiseTests.swift b/ios/MullvadVPNTests/PromiseTests.swift new file mode 100644 index 0000000000..616d492a3d --- /dev/null +++ b/ios/MullvadVPNTests/PromiseTests.swift @@ -0,0 +1,147 @@ +// +// PromiseTests.swift +// PromiseTests +// +// Created by pronebird on 22/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class PromiseTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testObserveResolvedPromise() throws { + let expect = expectation(description: "Wait for promise") + + Promise(value: 1) + .observe { completion in + XCTAssertEqual(completion, .finished(1)) + expect.fulfill() + } + + wait(for: [expect], timeout: 1) + } + + func testObservePromise() throws { + let expect = expectation(description: "Wait for promise") + Promise<Int> { resolver in + resolver.resolve(value: 1) + } + .observe { completion in + XCTAssertEqual(completion, .finished(1)) + expect.fulfill() + } + + wait(for: [expect], timeout: 1) + } + + func testReceiveOn() throws { + let expect = expectation(description: "Wait for promise") + let queue = DispatchQueue(label: "TestQueue") + + Promise(value: 1) + .receive(on: queue) + .observe { completion in + dispatchPrecondition(condition: .onQueue(queue)) + expect.fulfill() + } + + wait(for: [expect], timeout: 1) + } + + func testScheduleOn() throws { + let expect = expectation(description: "Wait for promise") + let queue = DispatchQueue(label: "TestQueue") + + Promise<Int> { resolver in + dispatchPrecondition(condition: .onQueue(queue)) + resolver.resolve(value: 1) + } + .schedule(on: queue) + .observe { completion in + expect.fulfill() + } + + wait(for: [expect], timeout: 1) + } + + func testBlockOn() throws { + let expect1 = expectation(description: "Wait for promise") + let expect2 = expectation(description: "Wait for queue to be unblocked") + let queue = DispatchQueue(label: "TestQueue") + + Promise<Int> { resolver in + DispatchQueue.main.async { + resolver.resolve(value: 1) + } + } + .block(on: queue) + .observe { completion in + dispatchPrecondition(condition: .onQueue(queue)) + expect1.fulfill() + } + + queue.async { + expect2.fulfill() + } + + wait(for: [expect1, expect2], timeout: 1, enforceOrder: true) + } + + func testCancellation() throws { + let cancelExpectation = expectation(description: "Expect cancellation handler to trigger") + let completionExpectation = expectation(description: "Expect promise to complete") + + let promise = Promise<Int> { resolver in + let work = DispatchWorkItem { + XCTFail() + resolver.resolve(value: 1) + } + + resolver.setCancelHandler { + work.cancel() + cancelExpectation.fulfill() + } + + DispatchQueue.main.async(execute: work) + }.observe { completion in + XCTAssertEqual(completion, .cancelled) + completionExpectation.fulfill() + } + + promise.cancel() + + wait(for: [cancelExpectation, completionExpectation], timeout: 1) + } + + func testOptionalMapNoneWithDefaultValue() { + let value: Int? = nil + + value.asPromise() + .map(defaultValue: 1) { _ in + return 2 + }.observe { completion in + XCTAssertEqual(completion.unwrappedValue, 1) + } + } + + func testOptionalMapSomeWithDefaultValue() { + let value: Int? = 0 + + value.asPromise() + .map(defaultValue: 1) { _ in + return 2 + }.observe { completion in + XCTAssertEqual(completion.unwrappedValue, 2) + } + } + +} |
