commit 29565094868a88a103526a7275bb816bb63c6f62
parent 0ae444daa24d60a1813a45086dbbdc138944af7c
Author: triesap <tyson@radroots.org>
Date: Fri, 19 Jun 2026 16:46:05 -0700
runtime: handle typed mobile read outcomes
- call throwing runtime reads from the Swift service boundary.
- surface profile no-data separately from typed profile read failures.
- keep post stream polling resilient to typed stream failures.
- lock iOS FFI generation to the runtime-read field_lib revision.
Diffstat:
6 files changed, 43 insertions(+), 24 deletions(-)
diff --git a/Radroots/Runtime/FieldRuntimeService.swift b/Radroots/Runtime/FieldRuntimeService.swift
@@ -82,8 +82,8 @@ public final class FieldRuntimeService: @unchecked Sendable {
try await run { try $0.nostrIdentityResetHostCustodyRuntime() }
}
- public func nostrProfileForSelf() async -> NostrProfileEventMetadata? {
- await runValue { $0.nostrProfileForSelf() }
+ public func nostrProfileForSelf() async throws -> NostrProfileEventMetadata? {
+ try await run { try $0.nostrProfileForSelf() }
}
public func nostrFetchTextNotes(
@@ -93,7 +93,7 @@ public final class FieldRuntimeService: @unchecked Sendable {
try await run { try $0.nostrFetchTextNotes(limit: limit, sinceUnix: sinceUnix) }
}
- public func nostrNextPostStreamEvent() async -> NostrPostEventMetadata? {
- await runValue { $0.nostrNextPostEvent() }
+ public func nostrNextPostStreamEvent() async throws -> NostrPostEventMetadata? {
+ try await run { try $0.nostrNextPostEvent() }
}
}
diff --git a/Radroots/Views/PostCreateView.swift b/Radroots/Views/PostCreateView.swift
@@ -93,7 +93,7 @@ struct PostCreateView: View {
}
} catch {
await MainActor.run {
- resultMessage = "Failed to post: \(error)"
+ resultMessage = "Failed to post: \(error.fieldRuntimeMessage)"
showResult = true
isPosting = false
}
diff --git a/Radroots/Views/PostFeedViewModel.swift b/Radroots/Views/PostFeedViewModel.swift
@@ -92,10 +92,11 @@ final class PostFeedViewModel: ObservableObject {
guard liveTask == nil else { return }
liveTask = Task { @MainActor [weak self] in
guard let self else { return }
+ guard let service = app.runtimeService else { return }
var knownIds = Set(posts.map(\.id))
let since = posts.map(\.publishedAt).max()
do {
- try await app.runtimeService?.nostrStartPostStream(sinceUnix: since)
+ try await service.nostrStartPostStream(sinceUnix: since)
} catch {
errorMessage = error.fieldRuntimeMessage
}
@@ -110,15 +111,20 @@ final class PostFeedViewModel: ObservableObject {
knownIds = Set(posts.map(\.id))
}
- if let event = await app.runtimeService?.nostrNextPostStreamEvent() {
- if knownIds.insert(event.id).inserted {
- posts.insert(event, at: 0)
- posts.sort { $0.publishedAt > $1.publishedAt }
- if posts.count > 200 {
- posts = Array(posts.prefix(200))
+ do {
+ if let event = try await service.nostrNextPostStreamEvent() {
+ if knownIds.insert(event.id).inserted {
+ posts.insert(event, at: 0)
+ posts.sort { $0.publishedAt > $1.publishedAt }
+ if posts.count > 200 {
+ posts = Array(posts.prefix(200))
+ }
}
+ } else {
+ try? await Task.sleep(for: .milliseconds(300))
}
- } else {
+ } catch {
+ errorMessage = error.fieldRuntimeMessage
try? await Task.sleep(for: .milliseconds(300))
}
}
diff --git a/Radroots/Views/ProfileView.swift b/Radroots/Views/ProfileView.swift
@@ -11,6 +11,7 @@ public struct ProfileView: View {
@State private var original: OriginalProfile = .empty
@State private var isLoading: Bool = false
@State private var isPosting: Bool = false
+ @State private var profileReadMessage: String?
@State private var postMessage: String?
@State private var showMessage: Bool = false
@FocusState private var focusedField: Field?
@@ -68,6 +69,9 @@ public struct ProfileView: View {
.animation(.easeInOut(duration: 0.15), value: hasChanges)
} footer: {
VStack(alignment: .leading, spacing: 6) {
+ if let profileReadMessage {
+ Text(profileReadMessage)
+ }
if !isConnected {
Text("No relays connected. Connect to at least one relay to post.")
}
@@ -111,14 +115,23 @@ public struct ProfileView: View {
guard let service = app.runtimeService else { return }
isLoading = true
Task {
- let meta = await service.nostrProfileForSelf()
- await MainActor.run {
- self.original = OriginalProfile.from(meta)
- self.name = original.name
- self.displayName = original.displayName
- self.nip05 = original.nip05
- self.about = original.about
- self.isLoading = false
+ do {
+ let meta = try await service.nostrProfileForSelf()
+ let loaded = OriginalProfile.from(meta)
+ await MainActor.run {
+ self.original = loaded
+ self.name = loaded.name
+ self.displayName = loaded.displayName
+ self.nip05 = loaded.nip05
+ self.about = loaded.about
+ self.profileReadMessage = meta == nil ? "No profile event found yet." : nil
+ self.isLoading = false
+ }
+ } catch {
+ await MainActor.run {
+ self.profileReadMessage = error.fieldRuntimeMessage
+ self.isLoading = false
+ }
}
}
}
@@ -146,7 +159,7 @@ public struct ProfileView: View {
} catch {
await MainActor.run {
self.isPosting = false
- self.postMessage = "Failed to post profile: \(error)"
+ self.postMessage = "Failed to post profile: \(error.fieldRuntimeMessage)"
self.showMessage = true
}
}
diff --git a/RadrootsFFI/Makefile b/RadrootsFFI/Makefile
@@ -6,7 +6,7 @@ SHELL := /bin/bash
SOURCE_MODE ?= git
RADROOTS_FIELD_LIB_GIT_URL ?= git@github.com:radrootslabs/field_lib.git
-RADROOTS_FIELD_LIB_GIT_REV ?= 20af1b91483cf420170bfb03d12d86da10f34363
+RADROOTS_FIELD_LIB_GIT_REV ?= 826c68898139e5119c6388fece76df6b853f6458
RADROOTS_FIELD_FFI_CRATE_VERSION ?= 0.1.0-alpha.1
FFI_FEATURES ?= radroots_field_core/rt,radroots_field_core/nostr-client
LOCAL_FFI_MANIFEST ?=
diff --git a/RadrootsFFI/source.lock b/RadrootsFFI/source.lock
@@ -1,5 +1,5 @@
SOURCE_MODE=git
RADROOTS_FIELD_LIB_GIT_URL=git@github.com:radrootslabs/field_lib.git
-RADROOTS_FIELD_LIB_GIT_REV=20af1b91483cf420170bfb03d12d86da10f34363
+RADROOTS_FIELD_LIB_GIT_REV=826c68898139e5119c6388fece76df6b853f6458
RADROOTS_FIELD_FFI_CRATE_VERSION=0.1.0-alpha.1
FFI_FEATURES=radroots_field_core/rt,radroots_field_core/nostr-client