fix: parallelize mobile-sync fan-out and skip known-offline peers#58
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR improves mobile sync broadcast latency by parallelizing per-peer fan-out and avoiding known-offline peers, and it also adds cross-platform push/analytics plumbing (Firebase + Android FCM) to support push delivery and unified event logging.
Changes:
- Parallelize relay socket broadcast + push fan-out using task groups to avoid N×RTT behavior.
- Introduce provider-aware push tokens (
apns/fcm) and add Android FCM push delivery end-to-end (Android app + relay + desktop). - Add a shared analytics facade (
AnalyticsService) and wire Firebase Analytics/Crashlytics bootstrapping; emit new navigation/surface analytics events.
Reviewed changes
Copilot reviewed 57 out of 58 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/ci/write-firebase-config.sh | Decodes Firebase config files from CI secrets into expected paths. |
| ci_scripts/ci_post_clone.sh | Calls Firebase config writer in Xcode Cloud post-clone step. |
| .github/workflows/test.yaml | Adds Firebase config materialization steps for CI jobs. |
| .github/workflows/build.yaml | Adds Firebase config materialization to build workflow. |
| .gitignore | Ignores decoded Firebase config files. |
| Packages/Sources/RxCodeCore/Utilities/AnalyticsService.swift | Adds transport-agnostic analytics service + shared event names. |
| RxCode/App/FirebaseBootstrap.swift | Configures Firebase + installs analytics handler (macOS app). |
| RxCodeMobile/FirebaseBootstrap.swift | Configures Firebase + installs analytics handler (iOS app). |
| RxCode/App/RxCodeApp.swift | Boots Firebase during macOS app init. |
| RxCodeMobile/AppDelegate.swift | Boots Firebase during iOS app launch. |
| RxCode/Views/Sidebar/BriefingView.swift | Logs briefing list open analytics event. |
| RxCodeMobile/Views/MobileBriefingView.swift | Logs briefing list open analytics event. |
| RxCodeMobile/Views/MobileBriefingDetailView.swift | UI tweaks + logs briefing detail open event. |
| RxCode/Views/RunProfile/RunProfileDetailForm.swift | Logs run profile editor open event. |
| RxCodeMobile/Views/MobileRunProfileEditorView.swift | Logs run profile editor open event. |
| RxCode/Views/Inspector/ChangesView.swift | Logs diff view open event from inspector changes surface. |
| RxCode/Views/Inspector/ThisThreadDiffView.swift | Logs diff view open event from “this thread” surface. |
| RxCodeMobile/Views/ThreadChangesSheet.swift | Logs diff view open event from mobile thread changes sheet. |
| RxCodeMobile/Views/MobileChatView.swift | Logs thread opened event on session load task. |
| RxCodeMobile/Views/NewThreadSheet.swift | Logs thread created event on newly created session. |
| RxCodeMobile/Views/ProjectsSidebar.swift | Logs project opened + new project started analytics events. |
| RxCodeMobile/Views/MobileInAppBrowserView.swift | Logs in-app browser open analytics event. |
| RxCode/App/AppState+Project.swift | Logs project opened when selecting a project (desktop). |
| Packages/Sources/RxCodeSync/Protocol/Payload.swift | Adds push_token payload type to shared sync protocol. |
| Packages/Tests/RxCodeSyncTests/PayloadTests.swift | Adds round-trip test for push_token payload. |
| RxCode/Services/MobileSyncService.swift | Parallelizes websocket broadcast; adds provider-aware push fan-out + FCM push send path. |
| RxCode/Services/MobileSyncService+EventDispatch.swift | Persists provider-aware push token state from inbound payloads. |
| RxCode/Services/MobileSyncService+LiveActivity.swift | Makes push rejection logging resilient to missing reason. |
| RxCode/App/AppState+MobileSnapshots.swift | Skips known-offline peers when broadcasting snapshots. |
| RxCode/Views/Settings/MobileSettingsTab.swift | Shows push badge/provider label and supports FCM devices. |
| relay-server/push.go | Adds provider routing for /push (APNs vs FCM). |
| relay-server/fcm.go | Implements FCM HTTP v1 sender + service-account loading. |
| relay-server/main.go | Wires FCM sender initialization + exposes via /push and /healthz. |
| relay-server/health.go | Extends health response with fcm availability. |
| relay-server/README.md | Documents /push provider and FCM config/env. |
| relay-server/go.mod | Promotes github.com/golang-jwt/jwt/v4 to direct dependency. |
| relay-server/k8s/README.md | Documents Kubernetes FCM secret/config setup. |
| relay-server/k8s/configmap.yaml | Adds FCM_PROJECT_ID config entry. |
| RxCodeAndroid/gradle/libs.versions.toml | Adds Firebase BOM, libs, and Gradle plugins to version catalog. |
| RxCodeAndroid/build.gradle.kts | Adds google-services + crashlytics plugins to root plugin management. |
| RxCodeAndroid/app/build.gradle.kts | Conditionally applies google-services/crashlytics; adds Firebase deps. |
| RxCodeAndroid/app/src/main/AndroidManifest.xml | Registers FCM service + requests POST_NOTIFICATIONS permission. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/RxCodeApplication.kt | Initializes Firebase when configured; enables Crashlytics collection. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/push/RxCodeFirebaseMessagingService.kt | Handles FCM token refresh + decrypts incoming encrypted alerts into notifications. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/push/FcmTokenReporter.kt | Reports FCM token to paired desktops via sync channel. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/push/EncryptedAlert.kt | Adds encrypted alert envelope + plaintext models for FCM path. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/Payload.kt | Adds push_token payload to Android serializer and model. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/MainActivity.kt | Requests notification permission; routes notification-tap deep links into state. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileState.kt | Adds buffered pending notification session id for deep link routing. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileAppState.kt | Reports FCM token on pairing/cold start; buffers notification-tap routing; adds draft→real session redirects. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/RxCodeApp.kt | Consumes buffered notification deep link and routes UI to the session. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/sheets/NewThreadSheet.kt | Redesigns new-thread sheet; starts session via view model and returns draft id. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/sessions/SessionsScreen.kt | Updates new-thread flow to navigate using returned session id and requires non-null viewModel. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/projects/ProjectsPane.kt | Avoids draft-session cross-tab routing; navigates using explicit session id. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingPane.kt | Updates new-thread callback to use session id. |
| RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingDetailScreen.kt | Launches new-thread sheet from briefing detail; returns created session id. |
| RxCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | Adds Firebase iOS SDK pins to Xcode workspace dependencies. |
| RxCode.xcodeproj/project.pbxproj | Adds Firebase SPM dependency products to macOS + iOS app targets. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+16
to
27
| ScrollView(.vertical) { | ||
| VStack(alignment: .leading, spacing: 24) { | ||
| headerCard | ||
| summaryCard | ||
| threadsSection | ||
| } | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .padding(.horizontal, 16) | ||
| .padding(.vertical, 20) | ||
| } | ||
| .scrollBounceBehavior(.basedOnSize, axes: .horizontal) | ||
| .scrollContentBackground(.hidden) |
Comment on lines
+27
to
+42
| for (desktop in desktops) { | ||
| desktop.relayUrl?.let { client.setRelayUrl(it) } | ||
| client.addPeer(desktop.pubkeyHex) | ||
| client.start() | ||
| withTimeoutOrNull(CONNECT_TIMEOUT_MS) { | ||
| client.connectionState.first { it == RelayClient.ConnectionState.CONNECTED } | ||
| } | ||
| val sent = client.send( | ||
| Payload.PushToken(PushTokenPayload(provider = "fcm", token = token)), | ||
| desktop.pubkeyHex, | ||
| ) | ||
| Log.w( | ||
| TAG, | ||
| "FCM token report sent=$sent desktop=${desktop.pubkeyHex.take(12)} token=${token.take(12)}", | ||
| ) | ||
| } |
Comment on lines
41
to
43
| init() { | ||
| FirebaseBootstrap.configure() | ||
| try? Tips.configure([ |
e439fae to
21a1726
Compare
Sync broadcasts were O(peers × relay-RTT) because every paired device was serviced serially. With 5 paired peers on a remote relay (~150ms RTT) a single change event took close to a second wall-clock, and offline peers still burned a full round-trip per broadcast before the relay returned `delivery_failed`. - broadcastToAllClients: fan out concurrently via TaskGroup. - fanoutPush / fanoutAPNs: same — each push is an independent HTTPS POST so per-device RTT no longer compounds. - broadcastMobileSnapshots: skip peers already known offline; presence flips back to online on the next inbound, so a peer that returns still gets snapshots immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
21a1726 to
d1c33ae
Compare
Contributor
Author
|
🎉 This PR is included in version 1.12.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
broadcastToAllClientsso one slow relay RTT does not block the others.fanoutPushandfanoutAPNsfor the same reason.broadcastMobileSnapshotsso we don't burn a round-trip just to receivedelivery_failed.Why
A log from a session with 5 paired peers (2 on Local Relay, 3 on RxLab Hosted Relay) showed:
403) returnedrelay delivery failed.broadcastMobileSnapshotsdid not filter byonlineState, so each broadcast paid 5× RTT just to be told "already offline" 5 times.Changes
RxCode/Services/MobileSyncService.swiftbroadcastToAllClients: snapshot targets on the main actor, fan out withwithTaskGroupso each peer's websocket send awaits concurrently.fanoutPush: capture provider + push URL on the main actor, then run the per-device HTTPS POST in aTaskGroup.fanoutAPNs: same pattern.RxCode/App/AppState+MobileSnapshots.swiftbroadcastMobileSnapshots: pre-filterpairedDevicesbyonlineState != .offline. Presence flips back to.onlineon any inbound, so a peer that returns gets snapshots again immediately.Test plan
xcodebuild -project RxCode.xcodeproj -scheme RxCode -configuration Debug buildsucceeds.delivery failedfor that device until it sends an inbound again.onlineStateand resume snapshots.🤖 Generated with Claude Code