commit 4c9510c419ed1f3b23f404589abce08d763daa38
parent 42129ae36227092930d929864fdea0ebfbb805c9
Author: triesap <tyson@radroots.org>
Date: Sat, 8 Aug 2026 00:18:33 +0000
refactor(ios): own runtime lifecycle behind an actor
- confine generated UniFFI types to a dedicated adapter framework
- serialize start retry stop and subscription cancellation by generation
- bound independent change streams and preserve callback ordering
- add Swift 6 lifecycle concurrency and typed shutdown stress coverage
Diffstat:
16 files changed, 1470 insertions(+), 727 deletions(-)
diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj
@@ -7,61 +7,32 @@
objects = {
/* Begin PBXBuildFile section */
- 022DA21729F49893319717AA /* RelaysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D9496F9F05A4E79E73A247 /* RelaysView.swift */; };
- 049D620DD8C02816893BF765 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1472FFD63A33F3AEA6C6C /* AppState.swift */; };
- 04AA409CFECBA11BFC175C5C /* RadrootsFFI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD7B47A576C4D5CE9318D3E6 /* RadrootsFFI.xcframework */; };
- 1C6EA551530A46CA77BD9E1C /* FieldLocationCheckIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3541389124A31F5D701A45 /* FieldLocationCheckIn.swift */; };
- 1E5B41A3E1F9A7D68F63B079 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0274A0260D1C04F40C71AF /* HomeView.swift */; };
- 25654E50F9519809A237759D /* FieldUserPresenceGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65835E1C633C7B946C64D11 /* FieldUserPresenceGate.swift */; };
- 275D4D574BF3B3C1DD746CE7 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71A93F98C7B93188748B99B /* ProfileView.swift */; };
- 299A6111507F657670856F36 /* FieldCaptureIntake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC1D11E2E296B4B54E7E8A9 /* FieldCaptureIntake.swift */; };
- 2B3886FD26434A54F3726591 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8D19AA8D515FC8F5D2407378 /* Localizable.strings */; };
- 2B6ACA26689B355CECBFFB57 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A7641E5C643B4B36CFEDA8 /* SetupView.swift */; };
- 2FAE0FC43EB547F2CE7A567D /* FieldBackgroundExecutionUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59AC0543EF8335D691D56BD3 /* FieldBackgroundExecutionUITestProbe.swift */; };
- 360F23EFE80FDBDC6983FB15 /* AppEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D448C9655B708CA3FA8712B9 /* AppEntry.swift */; };
- 3A7FA9E5BCC7590B2EAC5349 /* RelaySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBB081610305940C7849C7C /* RelaySettings.swift */; };
- 3B6020E24A2DAD8ADFC2F155 /* BuildConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */; };
- 3FC570AC038C3DC575E5A3E7 /* FieldDocumentInterchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F2BDC19EF9435162BFF5EC /* FieldDocumentInterchange.swift */; };
- 5AECD474FB2F91855BDD79C0 /* PostFeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21554DA87EEC1E5C5F38365 /* PostFeedViewModel.swift */; };
- 5FB3E3E450EE7DFF30F3A005 /* FieldIdentityPolicyUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E150C6C18B2A06F2F3227C6 /* FieldIdentityPolicyUITestProbe.swift */; };
- 657BEA5AAFF129E10177FE63 /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63189EB90A86A9929BECD9ED /* Nostr.swift */; };
- 6E15D30653861F26AC45B501 /* FieldLocalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */; };
- 7C8DD424F3E3E0AB1B133863 /* RadrootsKitBindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC881872F750120A184F45E6 /* RadrootsKitBindings.swift */; };
- 7E650F6DA30931E310F842E2 /* FieldFileAccessUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D57E540BAB04F24619737 /* FieldFileAccessUITestProbe.swift */; };
- 7FD8FB018DA09568303194B2 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE61264E2C98E73828E8680 /* Strings.swift */; };
- 82903551F5E15FBDAC388D20 /* FieldExternalActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6187FA7C4786EC662718B2 /* FieldExternalActions.swift */; };
- 850F3C1E444A4C0BC51A608D /* SharedEngineLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4ADF0DD703CA020D0CF67D /* SharedEngineLifecycleTests.swift */; };
- 8B923F78BF5B680C7F6A7CE3 /* PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE0EB327C10171444553378 /* PostFeedView.swift */; };
- 8F6D0970610DF68816DE1A98 /* Radroots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0F21496E7A8490EB14AC5B /* Radroots.swift */; };
- 9121BD4A3E7C6EF2B21F540F /* CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7DE4207398DE242519F9C /* CopyButton.swift */; };
- 9346DA48630668A65D37E14A /* FieldTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4E53FD4C4AADF63855888A /* FieldTelemetry.swift */; };
- 98FD8B614B481333C3F7708E /* FieldIdentityImportFailureUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 481E7791F4F6CF82FA3117C1 /* FieldIdentityImportFailureUITestProbe.swift */; };
- A1B921027DA7ACD7343BE250 /* SectionWideButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA8F5611075F60F214A00 /* SectionWideButton.swift */; };
- A54E244A554EC6B46DF8DE48 /* RadrootsAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D317BC9F759709098490DD /* RadrootsAppDelegate.swift */; };
- ABBA5CC10933CA087E14A0E8 /* FieldBackgroundExecution.swift in Sources */ = {isa = PBXBuildFile; fileRef = C614F2A8813E63C85261F492 /* FieldBackgroundExecution.swift */; };
- B8A3BBDE3A1FC0248512BF76 /* LoggingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DE3DD8C3BB2F63676F463E /* LoggingSettings.swift */; };
- B971351ABE8E79A472B4DC7D /* View+Nav.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B0C9861CD86EAD3CAD549E /* View+Nav.swift */; };
- C22DB0F3EB2E69A34DF941E0 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2818363B157125491FB84A1E /* App.swift */; };
- C8AB3389F7430A5C79AD7DF8 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */; };
- CC5561169A29B5B2B6423959 /* FieldTelemetryUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E6A1827AB4419F806CD848F /* FieldTelemetryUITestProbe.swift */; };
- D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */; };
- D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A4289F43625DD65E6C4B25 /* CopyRow.swift */; };
- D3E08BD0EB07C4E687BDAEF0 /* FieldBackgroundURLSessionEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA5C6DE4731C46D53B757E3 /* FieldBackgroundURLSessionEvents.swift */; };
- D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */; };
- D57452A5B550E4832913AF02 /* FieldUITestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901B3694434DC803B37651E8 /* FieldUITestSupport.swift */; };
- D62E9461833A0AA5E622A1E6 /* ToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227028B4EBDC6703999FB9DA /* ToastModifier.swift */; };
- D9BF5BE7E4AB5EACBF342539 /* FieldSecureIdentityStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */; };
- DCE468F668A3C346E716B04C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CCF0F7B3C57D8D770F178329 /* Assets.xcassets */; };
- DDDA83E35D868FE927D2ED37 /* FieldRuntimeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32238D3DFB4465DEEDE6E003 /* FieldRuntimeError.swift */; };
- E1EDAEE6B182025ACAF754A6 /* RadrootsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15DBA726450712D6DE88E951 /* RadrootsProvider.swift */; };
- E3864E34D67BAD0744B93180 /* Bundle+Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA7BAA021EE13E829390B /* Bundle+Build.swift */; };
- E432FD39ECC8F03764EEED81 /* FieldDocumentInterchangeUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05B944D348DAA4EF28B463 /* FieldDocumentInterchangeUITestProbe.swift */; };
- EB7C19F62D7DAB9C044D53AA /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C0EFACAD213A69C12D5064 /* PostDetailView.swift */; };
- F32EFF00A8A852F76657FEE1 /* PostCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA8AAF0C0F1723860A8481E0 /* PostCreateView.swift */; };
+ 27F796571E92A6589E0111E3 /* RadrootsKitBindings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF8CEF5491290B1218ACFCE7 /* RadrootsKitBindings.framework */; };
+ 47D9564F81A5E7CEA86B759C /* RadrootsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57C4A7EB045E43F109C4DCA1 /* RadrootsProvider.swift */; };
+ 5DAE76BBC71E4A192E029785 /* RadrootsGeneratedRuntimeBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B14CE9958D4C71AFB41D2DD /* RadrootsGeneratedRuntimeBackend.swift */; };
+ 69326B7DA05C115D0055AD13 /* RadrootsKitBindings.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FF8CEF5491290B1218ACFCE7 /* RadrootsKitBindings.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 74082509C257766A166662FA /* RadrootsAppModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 343F115F383EB9C8A796AC9C /* RadrootsAppModel.swift */; };
+ 7527378BC2D91420121C1C9C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F432D5450B21DFADF99FACD1 /* Localizable.strings */; };
+ AA7D80A73CD6C22ED5080781 /* RadrootsKitBindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 879C89400666972D90996AE5 /* RadrootsKitBindings.swift */; };
+ B356719A54375706DBBDC118 /* RadrootsRuntimeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDB24124BB8917DBB49EC2C /* RadrootsRuntimeModels.swift */; };
+ C356F2A068722087ED91256C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 298DA81B0A000E307098C840 /* Assets.xcassets */; };
+ CA827A410369AC55B0FEE305 /* RuntimeStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9469790A906080D90F8735CB /* RuntimeStatusView.swift */; };
+ CAD7B38817DC65AAF19576F7 /* RadrootsFFI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD7B47A576C4D5CE9318D3E6 /* RadrootsFFI.xcframework */; };
+ CEED5B97CB1F94445162D662 /* RadrootsRuntimeClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0451E8A8521D320172FFA35 /* RadrootsRuntimeClientTests.swift */; };
+ E236CCF8D8F8AC12AF59932E /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397ED29DF8E9BF73B4BBFDEC /* App.swift */; };
+ ED326EC0067A550727A1680D /* AppEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0D9C4E6CAE584304521D08 /* AppEntry.swift */; };
F3E40E5A76B4EA19AC7603D2 /* RadrootsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2DAD90EBF8EB00ACDD7611CD /* RadrootsKit */; };
+ F7EEF54776B037D98F389AD6 /* RadrootsRuntimeClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F5216F7C18E2EE21B8528A /* RadrootsRuntimeClient.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
+ 51F4BD021D143679140FF35F /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 572C41532B5066EC7641561C /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 1C22115E511334B291A71594;
+ remoteInfo = RadrootsKitBindings;
+ };
5B242C3EC56D6F3216103D77 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 572C41532B5066EC7641561C /* Project object */;
@@ -71,74 +42,55 @@
};
/* End PBXContainerItemProxy section */
+/* Begin PBXCopyFilesBuildPhase section */
+ 271D40E26A903BC9F058633E /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 69326B7DA05C115D0055AD13 /* RadrootsKitBindings.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
/* Begin PBXFileReference section */
- 08FA88664E5E3ED3A24D56CC /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
- 0A0274A0260D1C04F40C71AF /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
- 138AA7BAA021EE13E829390B /* Bundle+Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Build.swift"; sourceTree = "<group>"; };
- 15DBA726450712D6DE88E951 /* RadrootsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsProvider.swift; sourceTree = "<group>"; };
- 16A7641E5C643B4B36CFEDA8 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
- 19D317BC9F759709098490DD /* RadrootsAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsAppDelegate.swift; sourceTree = "<group>"; };
- 227028B4EBDC6703999FB9DA /* ToastModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastModifier.swift; sourceTree = "<group>"; };
- 2818363B157125491FB84A1E /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
- 2F3541389124A31F5D701A45 /* FieldLocationCheckIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldLocationCheckIn.swift; sourceTree = "<group>"; };
+ 298DA81B0A000E307098C840 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 30A8A8321A8A261F6D1B480D /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
318804462AADCA05FB04ACD9 /* RadrootsTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = RadrootsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 32238D3DFB4465DEEDE6E003 /* FieldRuntimeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldRuntimeError.swift; sourceTree = "<group>"; };
- 3B4E53FD4C4AADF63855888A /* FieldTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldTelemetry.swift; sourceTree = "<group>"; };
- 3E6187FA7C4786EC662718B2 /* FieldExternalActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldExternalActions.swift; sourceTree = "<group>"; };
- 41A4289F43625DD65E6C4B25 /* CopyRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyRow.swift; sourceTree = "<group>"; };
- 481E7791F4F6CF82FA3117C1 /* FieldIdentityImportFailureUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldIdentityImportFailureUITestProbe.swift; sourceTree = "<group>"; };
- 491D57E540BAB04F24619737 /* FieldFileAccessUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldFileAccessUITestProbe.swift; sourceTree = "<group>"; };
- 4BC4B7D0BB4C6D8E4B0AA4AD /* radroots.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = radroots.xcconfig; sourceTree = "<group>"; };
- 4C4ADF0DD703CA020D0CF67D /* SharedEngineLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedEngineLifecycleTests.swift; sourceTree = "<group>"; };
- 4E150C6C18B2A06F2F3227C6 /* FieldIdentityPolicyUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldIdentityPolicyUITestProbe.swift; sourceTree = "<group>"; };
- 54EE5A34FE2086899F5B7568 /* radroots.git.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = radroots.git.xcconfig; sourceTree = "<group>"; };
- 59AC0543EF8335D691D56BD3 /* FieldBackgroundExecutionUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldBackgroundExecutionUITestProbe.swift; sourceTree = "<group>"; };
- 63189EB90A86A9929BECD9ED /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; };
- 676B89EB116B60AE8C2B4313 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = "<group>"; };
- 6FBB081610305940C7849C7C /* RelaySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySettings.swift; sourceTree = "<group>"; };
- 7C294E8EF50F5E1E73F5C135 /* Common.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = "<group>"; };
- 7CC1D11E2E296B4B54E7E8A9 /* FieldCaptureIntake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCaptureIntake.swift; sourceTree = "<group>"; };
- 8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldSecureIdentityStore.swift; sourceTree = "<group>"; };
- 8DA5C6DE4731C46D53B757E3 /* FieldBackgroundURLSessionEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldBackgroundURLSessionEvents.swift; sourceTree = "<group>"; };
- 8E6A1827AB4419F806CD848F /* FieldTelemetryUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldTelemetryUITestProbe.swift; sourceTree = "<group>"; };
- 8F0F21496E7A8490EB14AC5B /* Radroots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Radroots.swift; sourceTree = "<group>"; };
- 901B3694434DC803B37651E8 /* FieldUITestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldUITestSupport.swift; sourceTree = "<group>"; };
+ 343F115F383EB9C8A796AC9C /* RadrootsAppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsAppModel.swift; sourceTree = "<group>"; };
+ 397ED29DF8E9BF73B4BBFDEC /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
+ 3DDB24124BB8917DBB49EC2C /* RadrootsRuntimeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsRuntimeModels.swift; sourceTree = "<group>"; };
+ 43B0EA447E213CD1B96846D6 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
+ 4E0D9C4E6CAE584304521D08 /* AppEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEntry.swift; sourceTree = "<group>"; };
+ 57C4A7EB045E43F109C4DCA1 /* RadrootsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsProvider.swift; sourceTree = "<group>"; };
+ 57F5216F7C18E2EE21B8528A /* RadrootsRuntimeClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsRuntimeClient.swift; sourceTree = "<group>"; };
+ 5B14CE9958D4C71AFB41D2DD /* RadrootsGeneratedRuntimeBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsGeneratedRuntimeBackend.swift; sourceTree = "<group>"; };
+ 67A31CC210B73EC072BD3542 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
+ 879C89400666972D90996AE5 /* RadrootsKitBindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsKitBindings.swift; sourceTree = "<group>"; };
93AA285819DD1269C3EAD80A /* Radroots.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Radroots.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 93D729E070C32490545FA837 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
- 9AE0EB327C10171444553378 /* PostFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedView.swift; sourceTree = "<group>"; };
- 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldLocalState.swift; sourceTree = "<group>"; };
- A0B0C9861CD86EAD3CAD549E /* View+Nav.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Nav.swift"; sourceTree = "<group>"; };
- A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildConfig.swift; sourceTree = "<group>"; };
- ADE61264E2C98E73828E8680 /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
- B289F4B276245ABE083D777F /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
+ 9469790A906080D90F8735CB /* RuntimeStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeStatusView.swift; sourceTree = "<group>"; };
BD7B47A576C4D5CE9318D3E6 /* RadrootsFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = RadrootsFFI.xcframework; path = Radroots/Frameworks/RadrootsFFI.xcframework; sourceTree = "<group>"; };
- C17CA8F5611075F60F214A00 /* SectionWideButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionWideButton.swift; sourceTree = "<group>"; };
- C1D9496F9F05A4E79E73A247 /* RelaysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaysView.swift; sourceTree = "<group>"; };
- C614F2A8813E63C85261F492 /* FieldBackgroundExecution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldBackgroundExecution.swift; sourceTree = "<group>"; };
- C71A93F98C7B93188748B99B /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
- CA8AAF0C0F1723860A8481E0 /* PostCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCreateView.swift; sourceTree = "<group>"; };
- CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldIdentityPublicMetadataStore.swift; sourceTree = "<group>"; };
- CBE1472FFD63A33F3AEA6C6C /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
- CCF0F7B3C57D8D770F178329 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
- D448C9655B708CA3FA8712B9 /* AppEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEntry.swift; sourceTree = "<group>"; };
- D4DE3DD8C3BB2F63676F463E /* LoggingSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingSettings.swift; sourceTree = "<group>"; };
- D65835E1C633C7B946C64D11 /* FieldUserPresenceGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldUserPresenceGate.swift; sourceTree = "<group>"; };
- DC881872F750120A184F45E6 /* RadrootsKitBindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsKitBindings.swift; sourceTree = "<group>"; };
- E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
- E4F2BDC19EF9435162BFF5EC /* FieldDocumentInterchange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldDocumentInterchange.swift; sourceTree = "<group>"; };
- E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldRuntimeService.swift; sourceTree = "<group>"; };
- EF05B944D348DAA4EF28B463 /* FieldDocumentInterchangeUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldDocumentInterchangeUITestProbe.swift; sourceTree = "<group>"; };
- F21554DA87EEC1E5C5F38365 /* PostFeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedViewModel.swift; sourceTree = "<group>"; };
- F3C0EFACAD213A69C12D5064 /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; };
- F4C7DE4207398DE242519F9C /* CopyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyButton.swift; sourceTree = "<group>"; };
+ D0451E8A8521D320172FFA35 /* RadrootsRuntimeClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsRuntimeClientTests.swift; sourceTree = "<group>"; };
+ FF8CEF5491290B1218ACFCE7 /* RadrootsKitBindings.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RadrootsKitBindings.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
+ 044C68780F358BC2C58657B8 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CAD7B38817DC65AAF19576F7 /* RadrootsFFI.xcframework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
1384F9BE8BB29250A1B4A26A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 04AA409CFECBA11BFC175C5C /* RadrootsFFI.xcframework in Frameworks */,
+ 27F796571E92A6589E0111E3 /* RadrootsKitBindings.framework in Frameworks */,
F3E40E5A76B4EA19AC7603D2 /* RadrootsKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -146,119 +98,53 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
- 23C2D7FF63B61CD356979E82 /* App */ = {
- isa = PBXGroup;
- children = (
- 2818363B157125491FB84A1E /* App.swift */,
- D448C9655B708CA3FA8712B9 /* AppEntry.swift */,
- CBE1472FFD63A33F3AEA6C6C /* AppState.swift */,
- 19D317BC9F759709098490DD /* RadrootsAppDelegate.swift */,
- 15DBA726450712D6DE88E951 /* RadrootsProvider.swift */,
- );
- path = App;
- sourceTree = "<group>";
- };
31C88176C5674CFF5F9CFBEA /* RadrootsTests */ = {
isa = PBXGroup;
children = (
- 4C4ADF0DD703CA020D0CF67D /* SharedEngineLifecycleTests.swift */,
+ D0451E8A8521D320172FFA35 /* RadrootsRuntimeClientTests.swift */,
);
path = RadrootsTests;
sourceTree = "<group>";
};
- 579F407D96CCAFD4000EF363 /* Config */ = {
+ 401A3C62BEECB8BD5C500756 /* Config */ = {
isa = PBXGroup;
children = (
- 676B89EB116B60AE8C2B4313 /* Base.xcconfig */,
- 7C294E8EF50F5E1E73F5C135 /* Common.xcconfig */,
- B289F4B276245ABE083D777F /* Debug.xcconfig */,
- 08FA88664E5E3ED3A24D56CC /* Release.xcconfig */,
+ 30A8A8321A8A261F6D1B480D /* Debug.xcconfig */,
+ 43B0EA447E213CD1B96846D6 /* Release.xcconfig */,
);
- path = Config;
- sourceTree = "<group>";
- };
- 5FD6379AE27C57D02E8C7EE1 /* Radroots */ = {
- isa = PBXGroup;
- children = (
- 54EE5A34FE2086899F5B7568 /* radroots.git.xcconfig */,
- 4BC4B7D0BB4C6D8E4B0AA4AD /* radroots.xcconfig */,
- 23C2D7FF63B61CD356979E82 /* App */,
- 579F407D96CCAFD4000EF363 /* Config */,
- 902187CDBBF756677402D2BF /* Generated */,
- E9C466456E8A8EEB73EE47F5 /* Resources */,
- 688000357CB95AB5B3067911 /* Runtime */,
- 932F9ACAF6A7D30E34F2E375 /* Shared */,
- BD0E20D32DF34D9E7C3EBCD2 /* Views */,
- );
- path = Radroots;
+ name = Config;
+ path = Radroots/Config;
sourceTree = "<group>";
};
6240123423927396E47D6B3E /* Products */ = {
isa = PBXGroup;
children = (
93AA285819DD1269C3EAD80A /* Radroots.app */,
+ FF8CEF5491290B1218ACFCE7 /* RadrootsKitBindings.framework */,
318804462AADCA05FB04ACD9 /* RadrootsTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
- 65EC1C4AF7DC676E78603D52 /* Localisation */ = {
- isa = PBXGroup;
- children = (
- ADE61264E2C98E73828E8680 /* Strings.swift */,
- );
- path = Localisation;
- sourceTree = "<group>";
- };
- 688000357CB95AB5B3067911 /* Runtime */ = {
- isa = PBXGroup;
- children = (
- A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */,
- C614F2A8813E63C85261F492 /* FieldBackgroundExecution.swift */,
- 59AC0543EF8335D691D56BD3 /* FieldBackgroundExecutionUITestProbe.swift */,
- 8DA5C6DE4731C46D53B757E3 /* FieldBackgroundURLSessionEvents.swift */,
- 7CC1D11E2E296B4B54E7E8A9 /* FieldCaptureIntake.swift */,
- E4F2BDC19EF9435162BFF5EC /* FieldDocumentInterchange.swift */,
- EF05B944D348DAA4EF28B463 /* FieldDocumentInterchangeUITestProbe.swift */,
- 3E6187FA7C4786EC662718B2 /* FieldExternalActions.swift */,
- 491D57E540BAB04F24619737 /* FieldFileAccessUITestProbe.swift */,
- 481E7791F4F6CF82FA3117C1 /* FieldIdentityImportFailureUITestProbe.swift */,
- 4E150C6C18B2A06F2F3227C6 /* FieldIdentityPolicyUITestProbe.swift */,
- CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */,
- 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */,
- 2F3541389124A31F5D701A45 /* FieldLocationCheckIn.swift */,
- 32238D3DFB4465DEEDE6E003 /* FieldRuntimeError.swift */,
- E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */,
- 8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */,
- 3B4E53FD4C4AADF63855888A /* FieldTelemetry.swift */,
- 8E6A1827AB4419F806CD848F /* FieldTelemetryUITestProbe.swift */,
- 901B3694434DC803B37651E8 /* FieldUITestSupport.swift */,
- D65835E1C633C7B946C64D11 /* FieldUserPresenceGate.swift */,
- D4DE3DD8C3BB2F63676F463E /* LoggingSettings.swift */,
- 63189EB90A86A9929BECD9ED /* Nostr.swift */,
- 8F0F21496E7A8490EB14AC5B /* Radroots.swift */,
- 6FBB081610305940C7849C7C /* RelaySettings.swift */,
- );
- path = Runtime;
- sourceTree = "<group>";
- };
- 902187CDBBF756677402D2BF /* Generated */ = {
+ 64C889EFCEB42611797110F7 /* Runtime */ = {
isa = PBXGroup;
children = (
- DC881872F750120A184F45E6 /* RadrootsKitBindings.swift */,
+ 5B14CE9958D4C71AFB41D2DD /* RadrootsGeneratedRuntimeBackend.swift */,
+ 57F5216F7C18E2EE21B8528A /* RadrootsRuntimeClient.swift */,
+ 3DDB24124BB8917DBB49EC2C /* RadrootsRuntimeModels.swift */,
);
- path = Generated;
+ name = Runtime;
+ path = Radroots/Runtime;
sourceTree = "<group>";
};
- 932F9ACAF6A7D30E34F2E375 /* Shared */ = {
+ 94F94915631E6DC54AAB0B89 /* Resources */ = {
isa = PBXGroup;
children = (
- D46F444AD1818932F03AC6B6 /* Components */,
- 9D22575D1FAD99FE8B6FCE6C /* Extensions */,
- 65EC1C4AF7DC676E78603D52 /* Localisation */,
- C5BAA3C6E2D410F0C8475D89 /* Modifiers */,
+ 298DA81B0A000E307098C840 /* Assets.xcassets */,
+ CF740CDF6131C939A001727E /* Localizations */,
);
- path = Shared;
+ name = Resources;
+ path = Radroots/Resources;
sourceTree = "<group>";
};
97FA23F0FD7E25C1AF2585FB /* Frameworks */ = {
@@ -269,79 +155,80 @@
name = Frameworks;
sourceTree = "<group>";
};
- 9D22575D1FAD99FE8B6FCE6C /* Extensions */ = {
+ C22A3ECCB9E9AF9E57B74576 /* App */ = {
isa = PBXGroup;
children = (
- 138AA7BAA021EE13E829390B /* Bundle+Build.swift */,
- A0B0C9861CD86EAD3CAD549E /* View+Nav.swift */,
+ 397ED29DF8E9BF73B4BBFDEC /* App.swift */,
+ 4E0D9C4E6CAE584304521D08 /* AppEntry.swift */,
+ 343F115F383EB9C8A796AC9C /* RadrootsAppModel.swift */,
+ 57C4A7EB045E43F109C4DCA1 /* RadrootsProvider.swift */,
);
- path = Extensions;
- sourceTree = "<group>";
- };
- BD0E20D32DF34D9E7C3EBCD2 /* Views */ = {
- isa = PBXGroup;
- children = (
- 0A0274A0260D1C04F40C71AF /* HomeView.swift */,
- CA8AAF0C0F1723860A8481E0 /* PostCreateView.swift */,
- F3C0EFACAD213A69C12D5064 /* PostDetailView.swift */,
- 9AE0EB327C10171444553378 /* PostFeedView.swift */,
- F21554DA87EEC1E5C5F38365 /* PostFeedViewModel.swift */,
- C71A93F98C7B93188748B99B /* ProfileView.swift */,
- C1D9496F9F05A4E79E73A247 /* RelaysView.swift */,
- E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */,
- 16A7641E5C643B4B36CFEDA8 /* SetupView.swift */,
- );
- path = Views;
+ name = App;
+ path = Radroots/App;
sourceTree = "<group>";
};
C4F02317699AB4FA59315D05 = {
isa = PBXGroup;
children = (
- 5FD6379AE27C57D02E8C7EE1 /* Radroots */,
+ C22A3ECCB9E9AF9E57B74576 /* App */,
+ 401A3C62BEECB8BD5C500756 /* Config */,
+ D17ED36BEBE689794F31C2BB /* Generated */,
31C88176C5674CFF5F9CFBEA /* RadrootsTests */,
+ 94F94915631E6DC54AAB0B89 /* Resources */,
+ 64C889EFCEB42611797110F7 /* Runtime */,
+ E05061D0ADB560FA929B4D9A /* Views */,
97FA23F0FD7E25C1AF2585FB /* Frameworks */,
6240123423927396E47D6B3E /* Products */,
);
sourceTree = "<group>";
};
- C5BAA3C6E2D410F0C8475D89 /* Modifiers */ = {
+ CF740CDF6131C939A001727E /* Localizations */ = {
isa = PBXGroup;
children = (
- 227028B4EBDC6703999FB9DA /* ToastModifier.swift */,
- );
- path = Modifiers;
- sourceTree = "<group>";
- };
- C5BC6846E71297290E1EAA29 /* Localizations */ = {
- isa = PBXGroup;
- children = (
- 8D19AA8D515FC8F5D2407378 /* Localizable.strings */,
+ F432D5450B21DFADF99FACD1 /* Localizable.strings */,
);
path = Localizations;
sourceTree = "<group>";
};
- D46F444AD1818932F03AC6B6 /* Components */ = {
+ D17ED36BEBE689794F31C2BB /* Generated */ = {
isa = PBXGroup;
children = (
- F4C7DE4207398DE242519F9C /* CopyButton.swift */,
- 41A4289F43625DD65E6C4B25 /* CopyRow.swift */,
- C17CA8F5611075F60F214A00 /* SectionWideButton.swift */,
+ 879C89400666972D90996AE5 /* RadrootsKitBindings.swift */,
);
- path = Components;
+ name = Generated;
+ path = Radroots/Generated;
sourceTree = "<group>";
};
- E9C466456E8A8EEB73EE47F5 /* Resources */ = {
+ E05061D0ADB560FA929B4D9A /* Views */ = {
isa = PBXGroup;
children = (
- CCF0F7B3C57D8D770F178329 /* Assets.xcassets */,
- C5BC6846E71297290E1EAA29 /* Localizations */,
+ 9469790A906080D90F8735CB /* RuntimeStatusView.swift */,
);
- path = Resources;
+ name = Views;
+ path = Radroots/Views;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
+ 1C22115E511334B291A71594 /* RadrootsKitBindings */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 4709AFBD0CA5DE7FC73BFC95 /* Build configuration list for PBXNativeTarget "RadrootsKitBindings" */;
+ buildPhases = (
+ 21C962D4AD2665D5ED66E70F /* Sources */,
+ 044C68780F358BC2C58657B8 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = RadrootsKitBindings;
+ packageProductDependencies = (
+ );
+ productName = RadrootsKitBindings;
+ productReference = FF8CEF5491290B1218ACFCE7 /* RadrootsKitBindings.framework */;
+ productType = "com.apple.product-type.framework";
+ };
469EC50C49E7266DF1C2000B /* Radroots */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2513910CE2A2ED545A78A163 /* Build configuration list for PBXNativeTarget "Radroots" */;
@@ -350,10 +237,12 @@
78F7810CC77628CB591F8CD2 /* Sources */,
0FB0BFB846269AC353E4AD0E /* Resources */,
1384F9BE8BB29250A1B4A26A /* Frameworks */,
+ 271D40E26A903BC9F058633E /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
+ 3AF61EED138E0994701CAA79 /* PBXTargetDependency */,
);
name = Radroots;
packageProductDependencies = (
@@ -409,6 +298,7 @@
projectDirPath = "";
projectRoot = "";
targets = (
+ 1C22115E511334B291A71594 /* RadrootsKitBindings */,
469EC50C49E7266DF1C2000B /* Radroots */,
67B2EF8D69F6CEA9BE5A8B04 /* RadrootsTests */,
);
@@ -420,8 +310,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- DCE468F668A3C346E716B04C /* Assets.xcassets in Resources */,
- 2B3886FD26434A54F3726591 /* Localizable.strings in Resources */,
+ C356F2A068722087ED91256C /* Assets.xcassets in Resources */,
+ 7527378BC2D91420121C1C9C /* Localizable.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -450,11 +340,19 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
+ 21C962D4AD2665D5ED66E70F /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ AA7D80A73CD6C22ED5080781 /* RadrootsKitBindings.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
4948BBB44D48B961DE81F508 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 850F3C1E444A4C0BC51A608D /* SharedEngineLifecycleTests.swift in Sources */,
+ CEED5B97CB1F94445162D662 /* RadrootsRuntimeClientTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -462,59 +360,25 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- C22DB0F3EB2E69A34DF941E0 /* App.swift in Sources */,
- 360F23EFE80FDBDC6983FB15 /* AppEntry.swift in Sources */,
- 049D620DD8C02816893BF765 /* AppState.swift in Sources */,
- 3B6020E24A2DAD8ADFC2F155 /* BuildConfig.swift in Sources */,
- E3864E34D67BAD0744B93180 /* Bundle+Build.swift in Sources */,
- 9121BD4A3E7C6EF2B21F540F /* CopyButton.swift in Sources */,
- D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */,
- ABBA5CC10933CA087E14A0E8 /* FieldBackgroundExecution.swift in Sources */,
- 2FAE0FC43EB547F2CE7A567D /* FieldBackgroundExecutionUITestProbe.swift in Sources */,
- D3E08BD0EB07C4E687BDAEF0 /* FieldBackgroundURLSessionEvents.swift in Sources */,
- 299A6111507F657670856F36 /* FieldCaptureIntake.swift in Sources */,
- 3FC570AC038C3DC575E5A3E7 /* FieldDocumentInterchange.swift in Sources */,
- E432FD39ECC8F03764EEED81 /* FieldDocumentInterchangeUITestProbe.swift in Sources */,
- 82903551F5E15FBDAC388D20 /* FieldExternalActions.swift in Sources */,
- 7E650F6DA30931E310F842E2 /* FieldFileAccessUITestProbe.swift in Sources */,
- 98FD8B614B481333C3F7708E /* FieldIdentityImportFailureUITestProbe.swift in Sources */,
- 5FB3E3E450EE7DFF30F3A005 /* FieldIdentityPolicyUITestProbe.swift in Sources */,
- D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */,
- 6E15D30653861F26AC45B501 /* FieldLocalState.swift in Sources */,
- 1C6EA551530A46CA77BD9E1C /* FieldLocationCheckIn.swift in Sources */,
- DDDA83E35D868FE927D2ED37 /* FieldRuntimeError.swift in Sources */,
- D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */,
- D9BF5BE7E4AB5EACBF342539 /* FieldSecureIdentityStore.swift in Sources */,
- 9346DA48630668A65D37E14A /* FieldTelemetry.swift in Sources */,
- CC5561169A29B5B2B6423959 /* FieldTelemetryUITestProbe.swift in Sources */,
- D57452A5B550E4832913AF02 /* FieldUITestSupport.swift in Sources */,
- 25654E50F9519809A237759D /* FieldUserPresenceGate.swift in Sources */,
- 1E5B41A3E1F9A7D68F63B079 /* HomeView.swift in Sources */,
- B8A3BBDE3A1FC0248512BF76 /* LoggingSettings.swift in Sources */,
- 657BEA5AAFF129E10177FE63 /* Nostr.swift in Sources */,
- F32EFF00A8A852F76657FEE1 /* PostCreateView.swift in Sources */,
- EB7C19F62D7DAB9C044D53AA /* PostDetailView.swift in Sources */,
- 8B923F78BF5B680C7F6A7CE3 /* PostFeedView.swift in Sources */,
- 5AECD474FB2F91855BDD79C0 /* PostFeedViewModel.swift in Sources */,
- 275D4D574BF3B3C1DD746CE7 /* ProfileView.swift in Sources */,
- 8F6D0970610DF68816DE1A98 /* Radroots.swift in Sources */,
- A54E244A554EC6B46DF8DE48 /* RadrootsAppDelegate.swift in Sources */,
- 7C8DD424F3E3E0AB1B133863 /* RadrootsKitBindings.swift in Sources */,
- E1EDAEE6B182025ACAF754A6 /* RadrootsProvider.swift in Sources */,
- 3A7FA9E5BCC7590B2EAC5349 /* RelaySettings.swift in Sources */,
- 022DA21729F49893319717AA /* RelaysView.swift in Sources */,
- A1B921027DA7ACD7343BE250 /* SectionWideButton.swift in Sources */,
- C8AB3389F7430A5C79AD7DF8 /* SettingsView.swift in Sources */,
- 2B6ACA26689B355CECBFFB57 /* SetupView.swift in Sources */,
- 7FD8FB018DA09568303194B2 /* Strings.swift in Sources */,
- D62E9461833A0AA5E622A1E6 /* ToastModifier.swift in Sources */,
- B971351ABE8E79A472B4DC7D /* View+Nav.swift in Sources */,
+ E236CCF8D8F8AC12AF59932E /* App.swift in Sources */,
+ ED326EC0067A550727A1680D /* AppEntry.swift in Sources */,
+ 74082509C257766A166662FA /* RadrootsAppModel.swift in Sources */,
+ 5DAE76BBC71E4A192E029785 /* RadrootsGeneratedRuntimeBackend.swift in Sources */,
+ 47D9564F81A5E7CEA86B759C /* RadrootsProvider.swift in Sources */,
+ F7EEF54776B037D98F389AD6 /* RadrootsRuntimeClient.swift in Sources */,
+ B356719A54375706DBBDC118 /* RadrootsRuntimeModels.swift in Sources */,
+ CA827A410369AC55B0FEE305 /* RuntimeStatusView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
+ 3AF61EED138E0994701CAA79 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 1C22115E511334B291A71594 /* RadrootsKitBindings */;
+ targetProxy = 51F4BD021D143679140FF35F /* PBXContainerItemProxy */;
+ };
C4ECFE4B8C347EE1F784B9BA /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 469EC50C49E7266DF1C2000B /* Radroots */;
@@ -523,10 +387,10 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
- 8D19AA8D515FC8F5D2407378 /* Localizable.strings */ = {
+ F432D5450B21DFADF99FACD1 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
- 93D729E070C32490545FA837 /* en */,
+ 67A31CC210B73EC072BD3542 /* en */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -534,25 +398,84 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
+ 065194E2B92BBDACF3D27647 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "";
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"Radroots/Frameworks\"",
+ );
+ GENERATE_INFOPLIST_FILE = YES;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 18.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = org.radroots.RadrootsKitBindings;
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ SWIFT_STRICT_CONCURRENCY = minimal;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
5486087B252C6EA76ADD9BB8 /* Debug */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = B289F4B276245ABE083D777F /* Debug.xcconfig */;
+ baseConfigurationReference = 30A8A8321A8A261F6D1B480D /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
+ INFOPLIST_FILE = Radroots/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ SDKROOT = iphoneos;
+ SWIFT_VERSION = 6.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 762521A89ECC390CEE5533D2 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "";
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"\"Radroots/Frameworks\"",
);
- INFOPLIST_FILE = Radroots/Info.plist;
+ GENERATE_INFOPLIST_FILE = YES;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
+ PRODUCT_BUNDLE_IDENTIFIER = org.radroots.RadrootsKitBindings;
SDKROOT = iphoneos;
- SWIFT_VERSION = 6.0;
+ SKIP_INSTALL = YES;
+ SWIFT_STRICT_CONCURRENCY = minimal;
+ SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
@@ -560,6 +483,7 @@
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -576,14 +500,11 @@
};
D930E9391B5B63DB518922D2 /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 08FA88664E5E3ED3A24D56CC /* Release.xcconfig */;
+ baseConfigurationReference = 43B0EA447E213CD1B96846D6 /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
- FRAMEWORK_SEARCH_PATHS = (
- "$(inherited)",
- "\"Radroots/Frameworks\"",
- );
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
INFOPLIST_FILE = Radroots/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -600,6 +521,7 @@
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -745,6 +667,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
+ 4709AFBD0CA5DE7FC73BFC95 /* Build configuration list for PBXNativeTarget "RadrootsKitBindings" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 762521A89ECC390CEE5533D2 /* Debug */,
+ 065194E2B92BBDACF3D27647 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
D17883D6A48F29A2A4DFE795 /* Build configuration list for PBXProject "Radroots" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/Radroots/App/App.swift b/Radroots/App/App.swift
@@ -2,14 +2,10 @@ import SwiftUI
@main
struct RadrootsApp: App {
- @UIApplicationDelegateAdaptor(RadrootsAppDelegate.self) private var appDelegate
-
var body: some Scene {
WindowGroup {
RadrootsProvider {
- AppEntry {
- HomeView()
- }
+ AppEntry()
}
}
}
diff --git a/Radroots/App/AppEntry.swift b/Radroots/App/AppEntry.swift
@@ -1,139 +1,12 @@
import SwiftUI
-public struct AppEntry<Main: View>: View {
- @EnvironmentObject private var appState: AppState
- private let main: () -> Main
-
- public init(@ViewBuilder main: @escaping () -> Main) {
- self.main = main
- }
-
- public var body: some View {
- Group {
- switch appState.bootstrapPhase {
- case .idle, .starting:
- SplashView()
- case .failed(let message):
- StartupFailureView(message: message) {
- appState.retryStartup()
- }
- case .ready:
- if appState.canShowAppContent {
- main()
- } else {
- NavigationStack {
- SetupView()
- }
- }
- }
- }
- .accessibilityIdentifier("field_ios.app_entry")
- #if DEBUG
- .overlay(alignment: .topLeading) {
- if let probeValue = appState.fileAccessProbeValue {
- Color.clear
- .frame(width: 1, height: 1)
- .accessibilityElement()
- .accessibilityIdentifier("field_ios.file_access.probe")
- .accessibilityValue(probeValue)
- }
- if let probeValue = appState.documentInterchangeProbeValue {
- Color.clear
- .frame(width: 1, height: 1)
- .accessibilityElement()
- .accessibilityIdentifier("field_ios.document_interchange.probe")
- .accessibilityValue(probeValue)
- }
- if let probeValue = appState.identityPolicyProbeValue {
- Color.clear
- .frame(width: 1, height: 1)
- .accessibilityElement()
- .accessibilityIdentifier("field_ios.identity_policy.probe")
- .accessibilityValue(probeValue)
- }
- if let probeValue = appState.identityImportFailureProbeValue {
- Color.clear
- .frame(width: 1, height: 1)
- .accessibilityElement()
- .accessibilityIdentifier("field_ios.identity_import_failure.probe")
- .accessibilityValue(probeValue)
- }
- if let probeValue = appState.telemetryProbeValue {
- Color.clear
- .frame(width: 1, height: 1)
- .accessibilityElement()
- .accessibilityIdentifier("field_ios.telemetry.probe")
- .accessibilityValue(probeValue)
- }
- if let probeValue = appState.backgroundExecutionProbeValue {
- Color.clear
- .frame(width: 1, height: 1)
- .accessibilityElement()
- .accessibilityIdentifier("field_ios.background_execution.probe")
- .accessibilityValue(probeValue)
- }
- }
- #endif
- }
-}
-
-private struct StartupFailureView: View {
- let message: String
- let onRetry: () -> Void
+struct AppEntry: View {
+ @EnvironmentObject private var appModel: RadrootsAppModel
var body: some View {
- VStack(spacing: 18) {
- Spacer()
- Image(systemName: "exclamationmark.triangle.fill")
- .font(.system(size: 56, weight: .semibold))
- .foregroundStyle(.red)
- Text("Startup failed")
- .font(.title2.weight(.semibold))
- Text(message)
- .font(.footnote)
- .foregroundStyle(.secondary)
- .multilineTextAlignment(.center)
- .textSelection(.enabled)
- Spacer()
- Button {
- onRetry()
- } label: {
- Label("Retry", systemImage: "arrow.clockwise")
- .frame(maxWidth: .infinity)
- }
- .buttonStyle(.borderedProminent)
- .controlSize(.large)
- .accessibilityIdentifier("field_ios.bootstrap.retry")
- }
- .padding()
- .accessibilityIdentifier("field_ios.bootstrap.failed")
- }
-}
-
-private struct SplashView: View {
- private let splashGlyphSize: CGFloat = 160
-
- var body: some View {
- ZStack {
- Color("RadrootsSplashBackground")
- .ignoresSafeArea()
-
- Color.clear
- .accessibilityElement()
- .accessibilityLabel("Startup")
- .accessibilityIdentifier("field_ios.bootstrap")
-
- GeometryReader { proxy in
- Image("RadrootsSplashLogomark")
- .resizable()
- .scaledToFit()
- .frame(width: splashGlyphSize, height: splashGlyphSize)
- .position(x: proxy.size.width / 2, y: proxy.size.height / 2)
- .accessibilityLabel("Radroots")
- .accessibilityIdentifier("field_ios.splash.logo")
- }
- .ignoresSafeArea()
+ RuntimeStatusView(phase: appModel.phase) {
+ Task { await appModel.retry() }
}
- .accessibilityElement(children: .contain)
+ .accessibilityIdentifier("radroots.app_entry")
}
}
diff --git a/Radroots/App/RadrootsAppModel.swift b/Radroots/App/RadrootsAppModel.swift
@@ -0,0 +1,67 @@
+import Foundation
+
+@MainActor
+final class RadrootsAppModel: ObservableObject {
+ enum Phase: Equatable {
+ case starting
+ case identityRequired
+ case running(RadrootsRuntimeSnapshot)
+ case failed(RadrootsRuntimeFailure)
+ case stopped
+ }
+
+ @Published private(set) var phase: Phase = .starting
+
+ private let runtimeClient: RadrootsRuntimeClient
+ private let configuration: RadrootsRuntimeLaunchConfiguration?
+
+ init(
+ runtimeClient: RadrootsRuntimeClient = .production(),
+ configuration: RadrootsRuntimeLaunchConfiguration? = nil
+ ) {
+ self.runtimeClient = runtimeClient
+ self.configuration = configuration
+ }
+
+ func start() async {
+ guard let configuration else {
+ phase = .identityRequired
+ return
+ }
+ phase = .starting
+ do {
+ phase = try await .running(runtimeClient.start(configuration: configuration))
+ } catch let RadrootsRuntimeClientError.startup(failure) {
+ phase = .failed(failure)
+ } catch {
+ phase = .failed(
+ .local(
+ operation: "app.start",
+ code: "ios.app.start_failed",
+ safeMessage: "Radroots could not start."
+ )
+ )
+ }
+ }
+
+ func retry() async {
+ await start()
+ }
+
+ func stop() async {
+ do {
+ _ = try await runtimeClient.stop()
+ phase = .stopped
+ } catch let RadrootsRuntimeClientError.shutdown(failure) {
+ phase = .failed(failure)
+ } catch {
+ phase = .failed(
+ .local(
+ operation: "app.stop",
+ code: "ios.app.stop_failed",
+ safeMessage: "Radroots could not finish shutting down."
+ )
+ )
+ }
+ }
+}
diff --git a/Radroots/App/RadrootsProvider.swift b/Radroots/App/RadrootsProvider.swift
@@ -1,48 +1,28 @@
import SwiftUI
-import UIKit
-public struct RadrootsProvider<Content: View>: View {
+struct RadrootsProvider<Content: View>: View {
@Environment(\.scenePhase) private var scenePhase
- @StateObject private var appState = AppState()
- private let onStartupError: ((Error) -> Void)?
+ @StateObject private var appModel: RadrootsAppModel
private let content: () -> Content
- public init(
- onStartupError: ((Error) -> Void)? = nil,
+ init(
+ appModel: @autoclosure @escaping () -> RadrootsAppModel = RadrootsAppModel(),
@ViewBuilder content: @escaping () -> Content
) {
- self.onStartupError = onStartupError
+ _appModel = StateObject(wrappedValue: appModel())
self.content = content
}
- public var body: some View {
+ var body: some View {
content()
- .environmentObject(appState)
- .environmentObject(appState.radroots)
- .task {
- do {
- try await appState.start()
- } catch {
- onStartupError?(error)
- }
- }
+ .environmentObject(appModel)
+ .task { await appModel.start() }
.onChange(of: scenePhase) { _, phase in
- switch phase {
- case .active:
- appState.appDidBecomeActive()
- case .background:
- appState.appDidEnterBackground()
- case .inactive:
- break
- @unknown default:
- break
+ if phase == .active {
+ Task { await appModel.start() }
+ } else if phase == .background {
+ Task { await appModel.stop() }
}
}
- .onReceive(NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification)) { _ in
- Task { await appState.shutdown() }
- }
- .onDisappear {
- Task { await appState.shutdown() }
- }
}
}
diff --git a/Radroots/Runtime/FieldRuntimeError.swift b/Radroots/Runtime/FieldRuntimeError.swift
@@ -1,59 +0,0 @@
-import Foundation
-
-enum FieldRuntimeErrorCategory: String, Sendable {
- case initialization
- case identity
- case secureStore
- case relay
- case runtime
- case unsupported
- case internalFailure
-}
-
-extension RadrootsAppError {
- var fieldCategory: FieldRuntimeErrorCategory {
- switch self {
- case .Initialization(_):
- .initialization
- case .Sdk(let report):
- if report.capabilityId?.hasPrefix("signing.") == true {
- .identity
- } else if report.capabilityId?.hasPrefix("transport.") == true {
- .relay
- } else {
- .runtime
- }
- case .Runtime(_):
- .runtime
- case .Unsupported(_):
- .unsupported
- case .Internal(_):
- .internalFailure
- }
- }
-
- var fieldMessage: String {
- switch self {
- case .Initialization(let message),
- .Runtime(let message),
- .Unsupported(let message),
- .Internal(let message):
- message
- case .Sdk(let report):
- report.message
- }
- }
-}
-
-extension Error {
- var fieldRuntimeErrorCategory: FieldRuntimeErrorCategory? {
- (self as? RadrootsAppError)?.fieldCategory
- }
-
- var fieldRuntimeMessage: String {
- if let fieldError = self as? RadrootsAppError {
- return fieldError.fieldMessage
- }
- return localizedDescription
- }
-}
diff --git a/Radroots/Runtime/FieldRuntimeService.swift b/Radroots/Runtime/FieldRuntimeService.swift
@@ -1,99 +0,0 @@
-import Foundation
-
-public final class FieldRuntimeService: @unchecked Sendable {
- let runtime: RadrootsRuntime
- private let queue = DispatchQueue(label: "org.radroots.field_ios.runtime", qos: .userInitiated)
-
- public init(runtime: RadrootsRuntime) {
- self.runtime = runtime
- }
-
- func run<T>(_ work: @escaping @Sendable (RadrootsRuntime) throws -> T) async throws -> T {
- try await withCheckedThrowingContinuation { continuation in
- queue.async {
- do {
- continuation.resume(returning: try work(self.runtime))
- } catch {
- continuation.resume(throwing: error)
- }
- }
- }
- }
-
- func runValue<T>(_ work: @escaping @Sendable (RadrootsRuntime) -> T) async -> T {
- await withCheckedContinuation { continuation in
- queue.async {
- continuation.resume(returning: work(self.runtime))
- }
- }
- }
-
- public func infoJson() async -> String {
- await runValue { $0.infoJson() }
- }
-
- public func shutdown() async throws -> SdkShutdownRecord {
- try await runtime.shutdown()
- }
-
- public func nostrSetDefaultRelays(_ relays: [String]) async throws {
- try await run { try $0.nostrSetDefaultRelays(relays: relays) }
- }
-
- public func nostrConnectIfKeyPresent() async throws {
- try await run { try $0.nostrConnectIfKeyPresent() }
- }
-
- public func nostrConnectionStatus() async throws -> NostrConnectionStatus {
- try await runtime.nostrConnectionStatus()
- }
-
- public func nostrIdentitySnapshot() async throws -> NostrIdentitySnapshot {
- try await run { try $0.nostrIdentitySnapshot() }
- }
-
- public func nostrIdentityList() async throws -> [NostrIdentityRecord] {
- try await run { try $0.nostrIdentityList() }
- }
-
- public func nostrIdentityValidateHostCustodySecret(secretKey: String) async throws -> NostrHostCustodyIdentity {
- try await run { try $0.nostrIdentityValidateHostCustodySecret(secretKey: secretKey) }
- }
-
- public func nostrIdentityRestoreHostCustodySecret(
- secretKey: String,
- label: String?,
- makeSelected: Bool
- ) async throws -> NostrIdentityRecord {
- try await run {
- try $0.nostrIdentityRestoreHostCustodySecret(
- secretKey: secretKey,
- label: label,
- makeSelected: makeSelected
- )
- }
- }
-
- public func nostrIdentityRemove(identityId: String) async throws {
- try await run { try $0.nostrIdentityRemove(identityId: identityId) }
- }
-
- public func nostrIdentityLockHostCustodyRuntime() async throws {
- try await run { try $0.nostrIdentityLockHostCustodyRuntime() }
- }
-
- public func nostrIdentityResetHostCustodyRuntime() async throws {
- try await run { try $0.nostrIdentityResetHostCustodyRuntime() }
- }
-
- public func nostrProfileForSelf() async throws -> NostrProfileEventMetadata? {
- try await runtime.nostrProfileForSelf()
- }
-
- public func nostrFetchTextNotes(
- limit: UInt16,
- sinceUnix: UInt64?
- ) async throws -> [NostrPostEventMetadata] {
- try await runtime.nostrFetchTextNotes(limit: limit, sinceUnix: sinceUnix)
- }
-}
diff --git a/Radroots/Runtime/Radroots.swift b/Radroots/Runtime/Radroots.swift
@@ -1,50 +0,0 @@
-import Foundation
-
-@MainActor
-public final class Radroots: ObservableObject {
- public private(set) var runtime: RadrootsRuntime?
- public private(set) var runtimeService: FieldRuntimeService?
-
- public init() {}
-
- func start(
- bundleId: String = Bundle.main.bundleIdentifier ?? "unknown",
- version: String = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0",
- build: String = (Bundle.main.infoDictionary?["CFBundleVersion"] as? String) ?? "0",
- buildSha: String? = nil,
- telemetry: FieldTelemetry = .shared
- ) throws -> FieldRuntimeService {
- let settings = LoggingSettings.load()
- do {
- try settings.apply(bundleIdentifier: bundleId)
- } catch {
- throw FieldRuntimeLoggingError.initializationFailed(error.fieldRuntimeMessage)
- }
- telemetry.runtimeLoggingInitialized(settings: settings)
-
- let rt = try RadrootsRuntime()
- let resolvedSha = buildSha ?? (Bundle.main.object(forInfoDictionaryKey: "GIT_SHA") as? String)
- rt.setAppInfoPlatform(
- platform: "iOS",
- bundleId: bundleId,
- version: version,
- buildNumber: build,
- buildSha: resolvedSha
- )
- self.runtime = rt
- let service = FieldRuntimeService(runtime: rt)
- self.runtimeService = service
- return service
- }
-
- public func info() -> RuntimeInfo? {
- runtime?.info()
- }
-
- public func shutdown() async {
- guard let runtimeService else { return }
- _ = try? await runtimeService.shutdown()
- self.runtimeService = nil
- runtime = nil
- }
-}
diff --git a/Radroots/Runtime/RadrootsGeneratedRuntimeBackend.swift b/Radroots/Runtime/RadrootsGeneratedRuntimeBackend.swift
@@ -0,0 +1,239 @@
+import Foundation
+import RadrootsKitBindings
+
+private final class RadrootsGeneratedRuntimeObserver: RadrootsRuntimeObserver, @unchecked Sendable {
+ private let continuation: AsyncStream<RadrootsRuntimeChange>.Continuation
+
+ init(continuation: AsyncStream<RadrootsRuntimeChange>.Continuation) {
+ self.continuation = continuation
+ }
+
+ func onChange(change: FfiRuntimeChangeRecord) {
+ continuation.yield(
+ RadrootsRuntimeChange(
+ schemaVersion: change.schemaVersion,
+ generation: change.generation,
+ kind: change.kind.appValue,
+ entityID: change.entityId
+ )
+ )
+ }
+
+ func finish() {
+ continuation.finish()
+ }
+}
+
+private actor RadrootsGeneratedSubscriptionToken: RadrootsRuntimeSubscriptionToken {
+ private var handle: FfiSubscriptionHandle?
+ private var observer: RadrootsGeneratedRuntimeObserver?
+ private var deliveryTask: Task<Void, Never>?
+
+ init(
+ handle: FfiSubscriptionHandle,
+ observer: RadrootsGeneratedRuntimeObserver,
+ deliveryTask: Task<Void, Never>
+ ) {
+ self.handle = handle
+ self.observer = observer
+ self.deliveryTask = deliveryTask
+ }
+
+ func cancel() async {
+ guard let handle else { return }
+ self.handle = nil
+ observer?.finish()
+ observer = nil
+ handle.unsubscribe()
+ deliveryTask?.cancel()
+ await deliveryTask?.value
+ deliveryTask = nil
+ }
+}
+
+private final class RadrootsGeneratedRuntimeBackend: RadrootsRuntimeBackend, @unchecked Sendable {
+ private let runtime: RadrootsRuntime
+
+ init(runtime: RadrootsRuntime) {
+ self.runtime = runtime
+ }
+
+ func snapshot() async throws -> RadrootsRuntimeSnapshot {
+ do {
+ let identity = try runtime.identityStatus()
+ let relay = try runtime.sdkRelayStatus()
+ let info = runtime.info()
+ return RadrootsRuntimeSnapshot(
+ identity: RadrootsRuntimeIdentity(
+ publicKeyHex: identity.publicKey,
+ hostSignerConfigured: identity.hostSignerConfigured
+ ),
+ relay: relay.map { report in
+ RadrootsRelayStatus(
+ profile: report.profile,
+ state: report.state,
+ readAvailability: report.readAvailability,
+ writeAvailability: report.writeAvailability,
+ relays: report.relays.map { relay in
+ RadrootsRelayEndpointStatus(
+ url: relay.relayUrl,
+ access: relay.access,
+ readState: relay.readState,
+ writeState: relay.writeState,
+ readLastAttemptUnixMilliseconds: relay.readLastAttemptUnixMs,
+ writeLastAttemptUnixMilliseconds: relay.writeLastAttemptUnixMs,
+ readNextAttemptUnixMilliseconds: relay.readNextAttemptUnixMs,
+ writeNextAttemptUnixMilliseconds: relay.writeNextAttemptUnixMs
+ )
+ }
+ )
+ },
+ crateName: info.sdk.crateName,
+ crateVersion: info.sdk.crateVersion,
+ isClosed: info.sdkClosed
+ )
+ } catch {
+ throw Self.failure(from: error)
+ }
+ }
+
+ func subscribe(
+ bufferCapacity: Int,
+ receive: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
+ ) async throws -> any RadrootsRuntimeSubscriptionToken {
+ let pair = AsyncStream.makeStream(
+ of: RadrootsRuntimeChange.self,
+ bufferingPolicy: .bufferingNewest(bufferCapacity)
+ )
+ do {
+ let observer = RadrootsGeneratedRuntimeObserver(continuation: pair.continuation)
+ let handle = try runtime.subscribeChanges(observer: observer)
+ let deliveryTask = Task {
+ for await change in pair.stream {
+ guard !Task.isCancelled else { break }
+ await receive(change)
+ }
+ }
+ return RadrootsGeneratedSubscriptionToken(
+ handle: handle,
+ observer: observer,
+ deliveryTask: deliveryTask
+ )
+ } catch {
+ pair.continuation.finish()
+ throw Self.failure(from: error)
+ }
+ }
+
+ func shutdown() async throws -> RadrootsRuntimeShutdownReceipt {
+ do {
+ let receipt = try await runtime.shutdown()
+ return RadrootsRuntimeShutdownReceipt(
+ state: receipt.state,
+ alreadyClosed: receipt.alreadyClosed
+ )
+ } catch {
+ throw Self.failure(from: error)
+ }
+ }
+
+ static func start(
+ configuration: RadrootsRuntimeLaunchConfiguration
+ ) async throws -> RadrootsRuntimeBackendStart {
+ var createdRuntime: RadrootsRuntime?
+ do {
+ let runtime = try await RadrootsRuntime(
+ applicationSupportDirectory: configuration.applicationSupportDirectory,
+ publicKeyHex: configuration.publicKeyHex,
+ sourceGenerationHex: configuration.sourceGenerationHex,
+ sourceGenerationCreatedAtUnixMs: configuration.sourceGenerationCreatedAtUnixMilliseconds,
+ protectedData: configuration.protectedData.generatedValue
+ )
+ createdRuntime = runtime
+ runtime.setAppInfoPlatform(
+ platform: "iOS",
+ bundleId: configuration.app.bundleIdentifier,
+ version: configuration.app.version,
+ buildNumber: configuration.app.buildNumber,
+ buildSha: configuration.app.buildSHA
+ )
+
+ switch configuration.networkProfile {
+ case .publicNetwork:
+ try runtime.configurePublicRelays(writableRelays: configuration.writableRelays)
+ try runtime.configurePublicBlossom(origins: configuration.blossomOrigins)
+ case .simulator:
+ try runtime.configureSimulatorRelays(loopbackRelays: configuration.writableRelays)
+ try runtime.configureSimulatorBlossom(origins: configuration.blossomOrigins)
+ case .device:
+ try runtime.configureDeviceRelays(writableRelays: configuration.writableRelays)
+ try runtime.configureDeviceBlossom(origins: configuration.blossomOrigins)
+ }
+
+ let backend = RadrootsGeneratedRuntimeBackend(runtime: runtime)
+ return try await RadrootsRuntimeBackendStart(
+ backend: backend,
+ snapshot: backend.snapshot()
+ )
+ } catch {
+ if let createdRuntime {
+ _ = try? await createdRuntime.shutdown()
+ }
+ throw Self.failure(from: error)
+ }
+ }
+
+ private static func failure(from error: Error) -> RadrootsRuntimeFailure {
+ if case let RadrootsAppError.Failure(report) = error {
+ return RadrootsRuntimeFailure(
+ schemaVersion: report.schemaVersion,
+ code: report.code,
+ category: report.category,
+ retryable: report.retryable,
+ recoveryActions: report.recoveryActions,
+ operationID: report.operationId,
+ capabilityID: report.capabilityId,
+ safeMessage: report.safeMessage
+ )
+ }
+ if let failure = error as? RadrootsRuntimeFailure {
+ return failure
+ }
+ return .local(
+ operation: "generated.runtime",
+ code: "ios.generated_runtime.unexpected",
+ safeMessage: "The Radroots runtime could not complete the operation."
+ )
+ }
+}
+
+extension RadrootsRuntimeClient {
+ static func production() -> RadrootsRuntimeClient {
+ RadrootsRuntimeClient { configuration in
+ try await RadrootsGeneratedRuntimeBackend.start(configuration: configuration)
+ }
+ }
+}
+
+private extension RadrootsProtectedDataState {
+ var generatedValue: ProtectedDataAvailability {
+ switch self {
+ case .available: .available
+ case .unavailable: .unavailable
+ }
+ }
+}
+
+private extension FfiRuntimeChangeKind {
+ var appValue: RadrootsRuntimeChangeKind {
+ switch self {
+ case .initial: .initial
+ case .identity: .identity
+ case .today: .today
+ case .drafts: .drafts
+ case .relay: .relay
+ case .media: .media
+ case .lifecycle: .lifecycle
+ }
+ }
+}
diff --git a/Radroots/Runtime/RadrootsRuntimeClient.swift b/Radroots/Runtime/RadrootsRuntimeClient.swift
@@ -0,0 +1,346 @@
+import Foundation
+
+protocol RadrootsRuntimeSubscriptionToken: Sendable {
+ func cancel() async
+}
+
+protocol RadrootsRuntimeBackend: Sendable {
+ func snapshot() async throws -> RadrootsRuntimeSnapshot
+ func subscribe(
+ bufferCapacity: Int,
+ receive: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
+ ) async throws -> any RadrootsRuntimeSubscriptionToken
+ func shutdown() async throws -> RadrootsRuntimeShutdownReceipt
+}
+
+struct RadrootsRuntimeBackendStart: Sendable {
+ let backend: any RadrootsRuntimeBackend
+ let snapshot: RadrootsRuntimeSnapshot
+}
+
+typealias RadrootsRuntimeBackendFactory = @Sendable (
+ RadrootsRuntimeLaunchConfiguration
+) async throws -> RadrootsRuntimeBackendStart
+
+actor RadrootsRuntimeClient {
+ private struct StartupOperation: Sendable {
+ let generation: UInt64
+ let configuration: RadrootsRuntimeLaunchConfiguration
+ let task: Task<Result<RadrootsRuntimeBackendStart, RadrootsRuntimeFailure>, Never>
+ }
+
+ private struct ShutdownOperation: Sendable {
+ let generation: UInt64
+ let task: Task<Result<RadrootsRuntimeShutdownReceipt, RadrootsRuntimeFailure>, Never>
+ }
+
+ private struct Subscription {
+ let generation: UInt64
+ let continuation: AsyncStream<RadrootsRuntimeChange>.Continuation
+ var token: (any RadrootsRuntimeSubscriptionToken)?
+ }
+
+ private let factory: RadrootsRuntimeBackendFactory
+ private var generation: UInt64 = 0
+ private var lifecycleState: RadrootsRuntimeLifecycle = .stopped
+ private var configuration: RadrootsRuntimeLaunchConfiguration?
+ private var backend: (any RadrootsRuntimeBackend)?
+ private var startupOperation: StartupOperation?
+ private var shutdownOperation: ShutdownOperation?
+ private var subscriptions: [UUID: Subscription] = [:]
+
+ init(factory: @escaping RadrootsRuntimeBackendFactory) {
+ self.factory = factory
+ }
+
+ func lifecycle() -> RadrootsRuntimeLifecycle {
+ lifecycleState
+ }
+
+ func start(
+ configuration requestedConfiguration: RadrootsRuntimeLaunchConfiguration
+ ) async throws -> RadrootsRuntimeSnapshot {
+ if let shutdownOperation {
+ _ = try await finishShutdown(shutdownOperation)
+ }
+
+ if let backend,
+ configuration == requestedConfiguration,
+ case .running = lifecycleState
+ {
+ return try await backendSnapshot(backend)
+ }
+
+ if let startupOperation,
+ startupOperation.configuration == requestedConfiguration
+ {
+ return try await finishStartup(startupOperation)
+ }
+
+ if startupOperation != nil || backend != nil {
+ let operation = beginShutdown()
+ _ = try await finishShutdown(operation)
+ }
+
+ generation &+= 1
+ let operationGeneration = generation
+ lifecycleState = .starting(generation: operationGeneration)
+
+ let factory = factory
+ let task = Task<Result<RadrootsRuntimeBackendStart, RadrootsRuntimeFailure>, Never> {
+ do {
+ return try await .success(factory(requestedConfiguration))
+ } catch {
+ return .failure(Self.failure(from: error, operation: "runtime.start"))
+ }
+ }
+ let operation = StartupOperation(
+ generation: operationGeneration,
+ configuration: requestedConfiguration,
+ task: task
+ )
+ startupOperation = operation
+ return try await finishStartup(operation)
+ }
+
+ func retry(
+ configuration requestedConfiguration: RadrootsRuntimeLaunchConfiguration
+ ) async throws -> RadrootsRuntimeSnapshot {
+ _ = try await stop()
+ return try await start(configuration: requestedConfiguration)
+ }
+
+ func snapshot() async throws -> RadrootsRuntimeSnapshot {
+ guard let backend, case .running = lifecycleState else {
+ throw RadrootsRuntimeClientError.notRunning
+ }
+ return try await backendSnapshot(backend)
+ }
+
+ func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<RadrootsRuntimeChange> {
+ guard (1 ... 64).contains(bufferCapacity) else {
+ throw RadrootsRuntimeClientError.invalidBufferCapacity
+ }
+ guard let backend, case .running = lifecycleState else {
+ throw RadrootsRuntimeClientError.notRunning
+ }
+
+ let id = UUID()
+ let subscriptionGeneration = generation
+ let pair = AsyncStream.makeStream(
+ of: RadrootsRuntimeChange.self,
+ bufferingPolicy: .bufferingNewest(bufferCapacity)
+ )
+ pair.continuation.onTermination = { [weak self] _ in
+ Task {
+ await self?.cancelSubscription(id: id, generation: subscriptionGeneration)
+ }
+ }
+ subscriptions[id] = Subscription(
+ generation: subscriptionGeneration,
+ continuation: pair.continuation,
+ token: nil
+ )
+
+ do {
+ let token = try await backend.subscribe(bufferCapacity: bufferCapacity) { [weak self] change in
+ await self?.receive(
+ change,
+ subscriptionID: id,
+ generation: subscriptionGeneration
+ )
+ }
+ guard generation == subscriptionGeneration,
+ var subscription = subscriptions[id]
+ else {
+ await token.cancel()
+ pair.continuation.finish()
+ throw RadrootsRuntimeClientError.superseded
+ }
+ subscription.token = token
+ subscriptions[id] = subscription
+ return pair.stream
+ } catch let error as RadrootsRuntimeClientError {
+ subscriptions.removeValue(forKey: id)?.continuation.finish()
+ throw error
+ } catch {
+ subscriptions.removeValue(forKey: id)?.continuation.finish()
+ throw RadrootsRuntimeClientError.subscription(
+ Self.failure(from: error, operation: "runtime.subscribe")
+ )
+ }
+ }
+
+ func stop() async throws -> RadrootsRuntimeShutdownReceipt {
+ if let shutdownOperation {
+ return try await finishShutdown(shutdownOperation)
+ }
+ guard startupOperation != nil || backend != nil || !subscriptions.isEmpty else {
+ lifecycleState = .stopped
+ return .alreadyStopped
+ }
+ return try await finishShutdown(beginShutdown())
+ }
+
+ private func finishStartup(
+ _ operation: StartupOperation
+ ) async throws -> RadrootsRuntimeSnapshot {
+ let result = await operation.task.value
+ guard generation == operation.generation else {
+ throw RadrootsRuntimeClientError.superseded
+ }
+
+ if let active = startupOperation,
+ active.generation == operation.generation
+ {
+ startupOperation = nil
+ } else if let backend,
+ configuration == operation.configuration,
+ case .running = lifecycleState
+ {
+ return try await backendSnapshot(backend)
+ } else {
+ throw RadrootsRuntimeClientError.superseded
+ }
+
+ switch result {
+ case let .success(started):
+ backend = started.backend
+ configuration = operation.configuration
+ lifecycleState = .running(generation: operation.generation)
+ return started.snapshot
+ case let .failure(failure):
+ lifecycleState = .failed(generation: operation.generation, failure: failure)
+ throw RadrootsRuntimeClientError.startup(failure)
+ }
+ }
+
+ private func beginShutdown() -> ShutdownOperation {
+ generation &+= 1
+ let operationGeneration = generation
+ let pendingStartup = startupOperation
+ let activeBackend = backend
+ let activeSubscriptions = Array(subscriptions.values)
+
+ startupOperation = nil
+ backend = nil
+ configuration = nil
+ subscriptions.removeAll()
+ lifecycleState = .stopping(generation: operationGeneration)
+
+ let task = Task<Result<RadrootsRuntimeShutdownReceipt, RadrootsRuntimeFailure>, Never> {
+ for subscription in activeSubscriptions {
+ subscription.continuation.finish()
+ await subscription.token?.cancel()
+ }
+
+ let backendToClose: (any RadrootsRuntimeBackend)?
+ if let activeBackend {
+ backendToClose = activeBackend
+ } else if let pendingStartup {
+ switch await pendingStartup.task.value {
+ case let .success(started):
+ backendToClose = started.backend
+ case let .failure(failure):
+ return .failure(failure)
+ }
+ } else {
+ backendToClose = nil
+ }
+
+ guard let backendToClose else {
+ return .success(.alreadyStopped)
+ }
+ do {
+ return try await .success(backendToClose.shutdown())
+ } catch {
+ return .failure(Self.failure(from: error, operation: "runtime.shutdown"))
+ }
+ }
+ let operation = ShutdownOperation(generation: operationGeneration, task: task)
+ shutdownOperation = operation
+ return operation
+ }
+
+ private func finishShutdown(
+ _ operation: ShutdownOperation
+ ) async throws -> RadrootsRuntimeShutdownReceipt {
+ let result = await operation.task.value
+ if shutdownOperation?.generation == operation.generation {
+ shutdownOperation = nil
+ switch result {
+ case .success:
+ lifecycleState = .stopped
+ case let .failure(failure):
+ lifecycleState = .failed(generation: operation.generation, failure: failure)
+ }
+ }
+
+ switch result {
+ case let .success(receipt):
+ return receipt
+ case let .failure(failure):
+ throw RadrootsRuntimeClientError.shutdown(failure)
+ }
+ }
+
+ private func backendSnapshot(
+ _ backend: any RadrootsRuntimeBackend
+ ) async throws -> RadrootsRuntimeSnapshot {
+ do {
+ return try await backend.snapshot()
+ } catch {
+ throw RadrootsRuntimeClientError.status(
+ Self.failure(from: error, operation: "runtime.status")
+ )
+ }
+ }
+
+ private func receive(
+ _ change: RadrootsRuntimeChange,
+ subscriptionID: UUID,
+ generation subscriptionGeneration: UInt64
+ ) {
+ guard generation == subscriptionGeneration,
+ let subscription = subscriptions[subscriptionID],
+ subscription.generation == subscriptionGeneration
+ else {
+ return
+ }
+ subscription.continuation.yield(change)
+ }
+
+ private func cancelSubscription(id: UUID, generation subscriptionGeneration: UInt64) async {
+ guard let subscription = subscriptions[id],
+ subscription.generation == subscriptionGeneration
+ else {
+ return
+ }
+ subscriptions.removeValue(forKey: id)
+ subscription.continuation.finish()
+ await subscription.token?.cancel()
+ }
+
+ private static func failure(from error: Error, operation: String) -> RadrootsRuntimeFailure {
+ if let failure = error as? RadrootsRuntimeFailure {
+ return failure
+ }
+ if case let RadrootsRuntimeClientError.startup(failure) = error {
+ return failure
+ }
+ if case let RadrootsRuntimeClientError.subscription(failure) = error {
+ return failure
+ }
+ if case let RadrootsRuntimeClientError.status(failure) = error {
+ return failure
+ }
+ if case let RadrootsRuntimeClientError.shutdown(failure) = error {
+ return failure
+ }
+ return .local(
+ operation: operation,
+ code: "ios.runtime.unexpected",
+ safeMessage: "The Radroots runtime could not complete the operation."
+ )
+ }
+}
diff --git a/Radroots/Runtime/RadrootsRuntimeModels.swift b/Radroots/Runtime/RadrootsRuntimeModels.swift
@@ -0,0 +1,153 @@
+import Foundation
+
+enum RadrootsProtectedDataState: Sendable, Equatable {
+ case available
+ case unavailable
+}
+
+enum RadrootsRuntimeNetworkProfile: Sendable, Equatable {
+ case publicNetwork
+ case simulator
+ case device
+}
+
+struct RadrootsRuntimeAppMetadata: Sendable, Equatable {
+ let bundleIdentifier: String
+ let version: String
+ let buildNumber: String
+ let buildSHA: String?
+}
+
+struct RadrootsRuntimeLaunchConfiguration: Sendable, Equatable {
+ let applicationSupportDirectory: String
+ let publicKeyHex: String
+ let sourceGenerationHex: String
+ let sourceGenerationCreatedAtUnixMilliseconds: UInt64
+ let protectedData: RadrootsProtectedDataState
+ let networkProfile: RadrootsRuntimeNetworkProfile
+ let writableRelays: [String]
+ let blossomOrigins: [String]
+ let app: RadrootsRuntimeAppMetadata
+}
+
+struct RadrootsRuntimeIdentity: Sendable, Equatable {
+ let publicKeyHex: String
+ let hostSignerConfigured: Bool
+}
+
+struct RadrootsRelayEndpointStatus: Sendable, Equatable {
+ let url: String
+ let access: String
+ let readState: String
+ let writeState: String
+ let readLastAttemptUnixMilliseconds: UInt64?
+ let writeLastAttemptUnixMilliseconds: UInt64?
+ let readNextAttemptUnixMilliseconds: UInt64?
+ let writeNextAttemptUnixMilliseconds: UInt64?
+}
+
+struct RadrootsRelayStatus: Sendable, Equatable {
+ let profile: String
+ let state: String
+ let readAvailability: String
+ let writeAvailability: String
+ let relays: [RadrootsRelayEndpointStatus]
+}
+
+struct RadrootsRuntimeSnapshot: Sendable, Equatable {
+ let identity: RadrootsRuntimeIdentity
+ let relay: RadrootsRelayStatus?
+ let crateName: String
+ let crateVersion: String
+ let isClosed: Bool
+}
+
+enum RadrootsRuntimeChangeKind: Sendable, Equatable {
+ case initial
+ case identity
+ case today
+ case drafts
+ case relay
+ case media
+ case lifecycle
+}
+
+struct RadrootsRuntimeChange: Sendable, Equatable {
+ let schemaVersion: UInt16
+ let generation: UInt64
+ let kind: RadrootsRuntimeChangeKind
+ let entityID: String?
+}
+
+struct RadrootsRuntimeShutdownReceipt: Sendable, Equatable {
+ let state: String
+ let alreadyClosed: Bool
+
+ static let alreadyStopped = Self(state: "closed", alreadyClosed: true)
+}
+
+struct RadrootsRuntimeFailure: Error, Sendable, Equatable {
+ let schemaVersion: UInt16
+ let code: String
+ let category: String
+ let retryable: Bool
+ let recoveryActions: [String]
+ let operationID: String?
+ let capabilityID: String?
+ let safeMessage: String
+
+ static func local(operation: String, code: String, safeMessage: String) -> Self {
+ Self(
+ schemaVersion: 1,
+ code: code,
+ category: "runtime",
+ retryable: false,
+ recoveryActions: [],
+ operationID: operation,
+ capabilityID: nil,
+ safeMessage: safeMessage
+ )
+ }
+}
+
+extension RadrootsRuntimeFailure: LocalizedError {
+ var errorDescription: String? {
+ safeMessage
+ }
+}
+
+enum RadrootsRuntimeClientError: Error, Sendable, Equatable {
+ case invalidBufferCapacity
+ case notRunning
+ case superseded
+ case startup(RadrootsRuntimeFailure)
+ case subscription(RadrootsRuntimeFailure)
+ case status(RadrootsRuntimeFailure)
+ case shutdown(RadrootsRuntimeFailure)
+}
+
+extension RadrootsRuntimeClientError: LocalizedError {
+ var errorDescription: String? {
+ switch self {
+ case .invalidBufferCapacity:
+ "The requested runtime event buffer is outside the supported range."
+ case .notRunning:
+ "The Radroots runtime is not running."
+ case .superseded:
+ "A newer runtime lifecycle request replaced this request."
+ case let .startup(failure),
+ let .subscription(failure),
+ let .status(failure),
+ let .shutdown(failure):
+ failure.safeMessage
+ }
+ }
+}
+
+enum RadrootsRuntimeLifecycle: Sendable, Equatable {
+ case stopped
+ case starting(generation: UInt64)
+ case running(generation: UInt64)
+ case stopping(generation: UInt64)
+ case failed(generation: UInt64, failure: RadrootsRuntimeFailure)
+}
diff --git a/Radroots/Views/RuntimeStatusView.swift b/Radroots/Views/RuntimeStatusView.swift
@@ -0,0 +1,76 @@
+import SwiftUI
+
+struct RuntimeStatusView: View {
+ let phase: RadrootsAppModel.Phase
+ let retry: () -> Void
+
+ var body: some View {
+ NavigationStack {
+ VStack(spacing: 20) {
+ Spacer()
+ Image(systemName: symbolName)
+ .font(.system(size: 48, weight: .medium))
+ .foregroundStyle(symbolColor)
+ .accessibilityHidden(true)
+ Text(title)
+ .font(.title2.weight(.semibold))
+ Text(detail)
+ .font(.body)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ if case .failed = phase {
+ Button("Retry", action: retry)
+ .buttonStyle(.borderedProminent)
+ .accessibilityIdentifier("radroots.runtime.retry")
+ }
+ Spacer()
+ }
+ .padding(24)
+ .navigationTitle("Radroots")
+ }
+ .accessibilityIdentifier("radroots.runtime.status")
+ }
+
+ private var symbolName: String {
+ switch phase {
+ case .starting: "leaf"
+ case .identityRequired: "person.badge.key"
+ case .running: "checkmark.circle"
+ case .failed: "exclamationmark.triangle"
+ case .stopped: "pause.circle"
+ }
+ }
+
+ private var symbolColor: Color {
+ switch phase {
+ case .failed: .red
+ case .running: .green
+ default: .accentColor
+ }
+ }
+
+ private var title: String {
+ switch phase {
+ case .starting: "Starting Radroots"
+ case .identityRequired: "Set up your identity"
+ case .running: "Radroots is ready"
+ case .failed: "Radroots needs attention"
+ case .stopped: "Radroots is paused"
+ }
+ }
+
+ private var detail: String {
+ switch phase {
+ case .starting:
+ "Preparing your local Radroots data."
+ case .identityRequired:
+ "Create or import an identity to connect your local food network."
+ case let .running(snapshot):
+ "Runtime \(snapshot.crateVersion) is connected to your local data."
+ case let .failed(failure):
+ failure.safeMessage
+ case .stopped:
+ "Your durable local work is safe."
+ }
+ }
+}
diff --git a/RadrootsFFI/source.lock b/RadrootsFFI/source.lock
@@ -8,4 +8,4 @@ override RADROOTS_FIELD_FFI_SWIFT_SHA256 := f9f7810554c17c4ce9e320dc32bd16911114
override RADROOTS_FIELD_FFI_HEADER_SHA256 := c85a8b87c225e2548f01a8e25dfaf772ae9c3e9c1262deb16c86c9043b05836a
override RADROOTS_FIELD_FFI_MODULEMAP_SHA256 := b228e67197e2c64b6f5b17a270faf640efc0e298e354b0b3301679e7c4dc321a
override RADROOTS_FIELD_FFI_API_SHA256 := 27d7f2b96a7bf55ab8d2b25effc6a10e149d5afc483cb1485e5e57254cd95339
-override RADROOTS_FIELD_FFI_XCFRAMEWORK_SHA256 := 24612315a9a2710b8e67d4c05651d2c128cdc5c5669f2ad69b574af6707d36c4
+override RADROOTS_FIELD_FFI_XCFRAMEWORK_SHA256 := 356708b157a2299ba7c3f36ecb93a1259d86c71617ce28af4c499bd554eb4965
diff --git a/RadrootsTests/RadrootsRuntimeClientTests.swift b/RadrootsTests/RadrootsRuntimeClientTests.swift
@@ -0,0 +1,317 @@
+@testable import Radroots
+import XCTest
+
+final class RadrootsRuntimeClientTests: XCTestCase {
+ func testConcurrentStartSharesOneBackend() async throws {
+ let harness = RuntimeHarness()
+ let client = RadrootsRuntimeClient(factory: harness.start)
+ let configuration = makeConfiguration(generation: "01")
+
+ let snapshots = try await withThrowingTaskGroup(of: RadrootsRuntimeSnapshot.self) { group in
+ for _ in 0 ..< 64 {
+ group.addTask {
+ try await client.start(configuration: configuration)
+ }
+ }
+ return try await group.reduce(into: []) { $0.append($1) }
+ }
+
+ XCTAssertEqual(snapshots.count, 64)
+ let startCount = await harness.startCount()
+ let lifecycle = await client.lifecycle()
+ XCTAssertEqual(startCount, 1)
+ XCTAssertEqual(lifecycle, .running(generation: 1))
+ _ = try await client.stop()
+ }
+
+ func testIndependentSubscriptionsUseBoundedNewestBuffers() async throws {
+ let harness = RuntimeHarness()
+ let client = RadrootsRuntimeClient(factory: harness.start)
+ _ = try await client.start(configuration: makeConfiguration(generation: "02"))
+ let first = try await client.changes(bufferCapacity: 2)
+ let second = try await client.changes(bufferCapacity: 4)
+
+ for generation in 1 ... 10 {
+ await harness.emit(change(generation: UInt64(generation)))
+ }
+
+ var firstIterator = first.makeAsyncIterator()
+ var secondIterator = second.makeAsyncIterator()
+ let firstValues = await [firstIterator.next(), firstIterator.next()].compactMap { $0?.generation }
+ let secondValues = await [
+ secondIterator.next(),
+ secondIterator.next(),
+ secondIterator.next(),
+ secondIterator.next(),
+ ].compactMap { $0?.generation }
+
+ XCTAssertEqual(firstValues, [9, 10])
+ XCTAssertEqual(secondValues, [7, 8, 9, 10])
+ _ = try await client.stop()
+ let cancelCount = await harness.cancelCount()
+ XCTAssertEqual(cancelCount, 2)
+ }
+
+ func testOverlappingStopsAwaitOneTypedShutdownFailure() async throws {
+ let failure = RadrootsRuntimeFailure.local(
+ operation: "test.shutdown",
+ code: "test.shutdown_failed",
+ safeMessage: "Shutdown did not complete."
+ )
+ let harness = RuntimeHarness(shutdownFailure: failure)
+ let client = RadrootsRuntimeClient(factory: harness.start)
+ _ = try await client.start(configuration: makeConfiguration(generation: "03"))
+
+ let results = await withTaskGroup(of: Result<RadrootsRuntimeShutdownReceipt, Error>.self) { group in
+ for _ in 0 ..< 32 {
+ group.addTask {
+ do {
+ return try await .success(client.stop())
+ } catch {
+ return .failure(error)
+ }
+ }
+ }
+ return await group.reduce(into: []) { $0.append($1) }
+ }
+
+ let shutdownCount = await harness.shutdownCount()
+ XCTAssertEqual(shutdownCount, 1)
+ XCTAssertEqual(results.count, 32)
+ for result in results {
+ guard case let .failure(error) = result else {
+ return XCTFail("Expected every waiter to receive the shutdown failure")
+ }
+ XCTAssertEqual(error as? RadrootsRuntimeClientError, .shutdown(failure))
+ }
+ }
+
+ func testNewConfigurationSupersedesAndClosesSlowStartup() async throws {
+ let harness = RuntimeHarness(startDelayNanoseconds: 30_000_000)
+ let client = RadrootsRuntimeClient(factory: harness.start)
+ let firstConfiguration = makeConfiguration(generation: "04")
+ let secondConfiguration = makeConfiguration(generation: "05")
+
+ let first = Task {
+ try await client.start(configuration: firstConfiguration)
+ }
+ try await Task.sleep(nanoseconds: 2_000_000)
+ let second = Task {
+ try await client.start(configuration: secondConfiguration)
+ }
+
+ do {
+ _ = try await first.value
+ XCTFail("The older startup should be superseded")
+ } catch {
+ XCTAssertEqual(error as? RadrootsRuntimeClientError, .superseded)
+ }
+ let snapshot = try await second.value
+ XCTAssertEqual(snapshot.identity.publicKeyHex, secondConfiguration.publicKeyHex)
+ let startCount = await harness.startCount()
+ let supersededShutdownCount = await harness.shutdownCount()
+ XCTAssertEqual(startCount, 2)
+ XCTAssertEqual(supersededShutdownCount, 1)
+ _ = try await client.stop()
+ let finalShutdownCount = await harness.shutdownCount()
+ XCTAssertEqual(finalShutdownCount, 2)
+ }
+
+ func testBufferCapacityAndStoppedSubscriptionFailClosed() async throws {
+ let harness = RuntimeHarness()
+ let client = RadrootsRuntimeClient(factory: harness.start)
+ do {
+ _ = try await client.changes()
+ XCTFail("A stopped runtime cannot subscribe")
+ } catch {
+ XCTAssertEqual(error as? RadrootsRuntimeClientError, .notRunning)
+ }
+
+ _ = try await client.start(configuration: makeConfiguration(generation: "06"))
+ for invalidCapacity in [0, 65] {
+ do {
+ _ = try await client.changes(bufferCapacity: invalidCapacity)
+ XCTFail("Invalid capacities must fail closed")
+ } catch {
+ XCTAssertEqual(error as? RadrootsRuntimeClientError, .invalidBufferCapacity)
+ }
+ }
+ _ = try await client.stop()
+ }
+
+ private func makeConfiguration(generation: String) -> RadrootsRuntimeLaunchConfiguration {
+ RadrootsRuntimeLaunchConfiguration(
+ applicationSupportDirectory: "/tmp/radroots-runtime-tests-\(generation)",
+ publicKeyHex: String(repeating: generation, count: 32),
+ sourceGenerationHex: String(repeating: generation, count: 32),
+ sourceGenerationCreatedAtUnixMilliseconds: 1,
+ protectedData: .available,
+ networkProfile: .simulator,
+ writableRelays: ["ws://127.0.0.1:7447"],
+ blossomOrigins: ["http://127.0.0.1:3000"],
+ app: RadrootsRuntimeAppMetadata(
+ bundleIdentifier: "org.radroots.tests",
+ version: "0.1.0-alpha",
+ buildNumber: "1",
+ buildSHA: nil
+ )
+ )
+ }
+
+ private func change(generation: UInt64) -> RadrootsRuntimeChange {
+ RadrootsRuntimeChange(
+ schemaVersion: 1,
+ generation: generation,
+ kind: .today,
+ entityID: "card-\(generation)"
+ )
+ }
+}
+
+private actor RuntimeHarness {
+ private let startDelayNanoseconds: UInt64
+ private let shutdownFailure: RadrootsRuntimeFailure?
+ private var starts = 0
+ private var shutdowns = 0
+ private var cancels = 0
+ private var receivers: [UUID: @Sendable (RadrootsRuntimeChange) async -> Void] = [:]
+
+ init(
+ startDelayNanoseconds: UInt64 = 0,
+ shutdownFailure: RadrootsRuntimeFailure? = nil
+ ) {
+ self.startDelayNanoseconds = startDelayNanoseconds
+ self.shutdownFailure = shutdownFailure
+ }
+
+ func start(
+ configuration: RadrootsRuntimeLaunchConfiguration
+ ) async throws -> RadrootsRuntimeBackendStart {
+ starts += 1
+ if startDelayNanoseconds > 0 {
+ try? await Task.sleep(nanoseconds: startDelayNanoseconds)
+ }
+ let backend = TestRuntimeBackend(
+ harness: self,
+ publicKeyHex: configuration.publicKeyHex,
+ shutdownFailure: shutdownFailure
+ )
+ return await RadrootsRuntimeBackendStart(
+ backend: backend,
+ snapshot: backend.snapshotValue()
+ )
+ }
+
+ func addReceiver(
+ _ receive: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
+ ) -> UUID {
+ let id = UUID()
+ receivers[id] = receive
+ return id
+ }
+
+ func removeReceiver(id: UUID) {
+ if receivers.removeValue(forKey: id) != nil {
+ cancels += 1
+ }
+ }
+
+ func emit(_ change: RadrootsRuntimeChange) async {
+ for receive in receivers.values {
+ await receive(change)
+ }
+ }
+
+ func recordShutdown() {
+ shutdowns += 1
+ }
+
+ func startCount() -> Int {
+ starts
+ }
+
+ func shutdownCount() -> Int {
+ shutdowns
+ }
+
+ func cancelCount() -> Int {
+ cancels
+ }
+}
+
+private actor TestRuntimeBackend: RadrootsRuntimeBackend {
+ private let harness: RuntimeHarness
+ private let snapshotValueStored: RadrootsRuntimeSnapshot
+ private let shutdownFailure: RadrootsRuntimeFailure?
+ private var closed = false
+
+ init(
+ harness: RuntimeHarness,
+ publicKeyHex: String,
+ shutdownFailure: RadrootsRuntimeFailure?
+ ) {
+ self.harness = harness
+ self.shutdownFailure = shutdownFailure
+ snapshotValueStored = RadrootsRuntimeSnapshot(
+ identity: RadrootsRuntimeIdentity(
+ publicKeyHex: publicKeyHex,
+ hostSignerConfigured: true
+ ),
+ relay: nil,
+ crateName: "radroots_mobile_ffi",
+ crateVersion: "0.1.0-alpha",
+ isClosed: false
+ )
+ }
+
+ func snapshotValue() -> RadrootsRuntimeSnapshot {
+ snapshotValueStored
+ }
+
+ func snapshot() throws -> RadrootsRuntimeSnapshot {
+ if closed {
+ throw RadrootsRuntimeFailure.local(
+ operation: "test.snapshot",
+ code: "test.closed",
+ safeMessage: "The test runtime is closed."
+ )
+ }
+ return snapshotValueStored
+ }
+
+ func subscribe(
+ bufferCapacity _: Int,
+ receive: @escaping @Sendable (RadrootsRuntimeChange) async -> Void
+ ) async -> any RadrootsRuntimeSubscriptionToken {
+ let id = await harness.addReceiver(receive)
+ return TestSubscriptionToken(harness: harness, id: id)
+ }
+
+ func shutdown() async throws -> RadrootsRuntimeShutdownReceipt {
+ await harness.recordShutdown()
+ try? await Task.sleep(nanoseconds: 10_000_000)
+ if let shutdownFailure {
+ throw shutdownFailure
+ }
+ let alreadyClosed = closed
+ closed = true
+ return RadrootsRuntimeShutdownReceipt(state: "closed", alreadyClosed: alreadyClosed)
+ }
+}
+
+private actor TestSubscriptionToken: RadrootsRuntimeSubscriptionToken {
+ private let harness: RuntimeHarness
+ private let id: UUID
+ private var cancelled = false
+
+ init(harness: RuntimeHarness, id: UUID) {
+ self.harness = harness
+ self.id = id
+ }
+
+ func cancel() async {
+ guard !cancelled else { return }
+ cancelled = true
+ await harness.removeReceiver(id: id)
+ }
+}
diff --git a/RadrootsTests/SharedEngineLifecycleTests.swift b/RadrootsTests/SharedEngineLifecycleTests.swift
@@ -1,50 +0,0 @@
-import XCTest
-@testable import Radroots
-
-final class SharedEngineLifecycleTests: XCTestCase {
- private let secret = String(repeating: "01", count: 32)
-
- func testIdentityRemainsHostCustodiedAndLockClearsRuntimeState() async throws {
- let service = try FieldRuntimeService(runtime: RadrootsRuntime())
- let validated = try await service.nostrIdentityValidateHostCustodySecret(secretKey: secret)
- let initialSnapshot = try await service.nostrIdentitySnapshot()
- XCTAssertFalse(initialSnapshot.hasSelectedSigningIdentity)
-
- let selected = try await service.nostrIdentityRestoreHostCustodySecret(
- secretKey: secret,
- label: "Simulator",
- makeSelected: true
- )
- XCTAssertEqual(selected.id, validated.id)
- let selectedSnapshot = try await service.nostrIdentitySnapshot()
- XCTAssertTrue(selectedSnapshot.hasSelectedSigningIdentity)
-
- try await service.nostrIdentityLockHostCustodyRuntime()
- let lockedSnapshot = try await service.nostrIdentitySnapshot()
- XCTAssertFalse(lockedSnapshot.hasSelectedSigningIdentity)
- _ = try await service.shutdown()
- }
-
- func testRelayStatusReportsCapabilitiesWithoutConnectionCounts() async throws {
- let service = try FieldRuntimeService(runtime: RadrootsRuntime())
- try await service.nostrSetDefaultRelays(["wss://relay.damus.io"])
-
- let status = try await service.nostrConnectionStatus()
- XCTAssertTrue(status.configured)
- XCTAssertTrue(status.sourceAvailable)
- XCTAssertTrue(status.sinkAvailable)
- XCTAssertNil(status.lastError)
- _ = try await service.shutdown()
- }
-
- func testShutdownIsExplicitAndIdempotent() async throws {
- let service = try FieldRuntimeService(runtime: RadrootsRuntime())
- let first = try await service.shutdown()
- let second = try await service.shutdown()
-
- XCTAssertEqual(first.state, "closed")
- XCTAssertFalse(first.alreadyClosed)
- XCTAssertEqual(second.state, "closed")
- XCTAssertTrue(second.alreadyClosed)
- }
-}
diff --git a/project.yml b/project.yml
@@ -14,25 +14,47 @@ packages:
revision: 265015fc537bfe507f12d801b9765be9764fbc95
targets:
+ RadrootsKitBindings:
+ type: framework
+ platform: iOS
+ deploymentTarget: "18.0"
+ sources:
+ - path: Radroots/Generated/RadrootsKitBindings.swift
+ settings:
+ base:
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]": x86_64
+ GENERATE_INFOPLIST_FILE: true
+ PRODUCT_BUNDLE_IDENTIFIER: org.radroots.RadrootsKitBindings
+ SWIFT_VERSION: 5.0
+ SWIFT_STRICT_CONCURRENCY: minimal
+ dependencies:
+ - framework: Radroots/Frameworks/RadrootsFFI.xcframework
+ embed: false
Radroots:
type: application
platform: iOS
deploymentTarget: "18.0"
sources:
- - path: Radroots
- excludes:
- - Info.plist
- - Frameworks
+ - path: Radroots/App/App.swift
+ - path: Radroots/App/AppEntry.swift
+ - path: Radroots/App/RadrootsAppModel.swift
+ - path: Radroots/App/RadrootsProvider.swift
+ - path: Radroots/Runtime/RadrootsGeneratedRuntimeBackend.swift
+ - path: Radroots/Runtime/RadrootsRuntimeClient.swift
+ - path: Radroots/Runtime/RadrootsRuntimeModels.swift
+ - path: Radroots/Views/RuntimeStatusView.swift
+ - path: Radroots/Resources
settings:
base:
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]": x86_64
SWIFT_VERSION: 6.0
INFOPLIST_FILE: Radroots/Info.plist
configFiles:
Debug: Radroots/Config/Debug.xcconfig
Release: Radroots/Config/Release.xcconfig
dependencies:
- - framework: Radroots/Frameworks/RadrootsFFI.xcframework
- embed: false
+ - target: RadrootsKitBindings
+ embed: true
- package: RadrootsKit
preBuildScripts:
- name: Generate git SHA xcconfig
@@ -45,9 +67,10 @@ targets:
platform: iOS
deploymentTarget: "18.0"
sources:
- - path: RadrootsTests
+ - path: RadrootsTests/RadrootsRuntimeClientTests.swift
settings:
base:
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]": x86_64
SWIFT_VERSION: 6.0
GENERATE_INFOPLIST_FILE: true
dependencies: