Skip to content

refactor: extract ProxyClient into AppState extension (Stage 3c) - #39

Merged
DocNR merged 1 commit into
mainfrom
refactor/proxy-client-extension
May 8, 2026
Merged

refactor: extract ProxyClient into AppState extension (Stage 3c)#39
DocNR merged 1 commit into
mainfrom
refactor/proxy-client-extension

Conversation

@DocNR

@DocNRDocNR commented May 8, 2026

Copy link
Copy Markdown
Owner

Summary

Stage 3c — last of the Stage 3 sub-extractions. Moves all proxy HTTP + NIP-98 + retry-queue logic out of AppState.swift into a dedicated extension file. The new file owns the [Pair] log category from PR #27.

Numbers

FileBeforeAfterΔ
Clave/AppState.swift1,483 LOC922 LOC−561 (−37.8%)
Clave/AppState+ProxyClient.swift577 LOC+577 (new)

Combined Stages 1+2+3a+3b+3c: AppState.swift down from 2,338 → 922 LOC (−60.6%). Within reach of the BACKLOG-sketched ~300 LOC slim AppState target after Stage 4.

What moved

Into Clave/AppState+ProxyClient.swift:

MethodVisibility
registerWithProxy(completion:)internal
registerAllAccountsWithProxy()internal
registerSignerWithProxy(signer:completion:)private
ensureRegisteredFresh()internal
ensureAllRegisteredFresh()internal
unregisterWithProxy(signer:)internal
pairClientWithProxy(...)internal
unpairClientWithProxy(...)internal
drainPendingPairOps()internal
retryPairOp(op:relayUrls:)private
retryUnpairOp(op:)private
// MARK: - Proxy per-client-relay (V2)header
private let logger = Logger(...category: "pair")file-scope

Logger relocation

The file-scope private let logger = Logger(subsystem: "dev.nostr.clave", category: "pair") (line 8 of AppState.swift) is relocated to the top of the new file. All 16 of its uses moved with the cluster — AppState.swift had zero remaining users after the extraction.

Multiple Logger instances with the same subsystem+category are equivalent — log streams combine. Dev-menu "Copy Recent Logs" via LogExporter.allCategories (which includes "pair") is unchanged.

Zero behavior change

  • Function bodies preserved byte-for-byte
  • Original private modifiers preserved on registerSignerWithProxy, retryPairOp, retryUnpairOp
  • All Task { @MainActor in } closures, URLSession callbacks, NIP-98 signing paths unchanged
  • Cross-extension call from AppState+NostrConnect.swift's handleNostrConnect to pairClientWithProxy still resolves correctly (both are extension AppState in the same module)

External callers unchanged

FileMethods called
Clave/ClaveApp.swiftdrainPendingPairOps (NotificationCenter observer)
Clave/Views/MainTabView.swiftensureRegisteredFresh, ensureAllRegisteredFresh
Clave/Views/Settings/SettingsView.swiftregisterWithProxy, unregisterWithProxy
Clave/Views/Onboarding/OnboardingView.swiftregisterWithProxy
Clave/Views/Home/HomeView.swiftregisterAllAccountsWithProxy
Clave/Views/Home/ClientDetailView.swiftunpairClientWithProxy

All call via appState.<method> — extension dispatch.

Test plan

  • xcodebuild test -skip-testing:ClaveUITests on iPhone 17 / iOS 26.4 against main (pre-baseline): 232 passed / 0 failed
  • Same command on this branch: 232 passed / 0 failed
  • ** TEST SUCCEEDED ** in both runs (no test count change)
  • After merge: separate chore/pbxproj-build-68 PR; archive build 68 + on-device verify of the proxy + NIP-46 hot path:
    • Cold launch → ensureRegisteredFresh fires (look for [Pair] pair-client begin/ok in logs if relevant)
    • Pair a fresh client → pairClientWithProxy registers URI relays with proxy
    • Sign one event (proxy must catch the request)
    • Settings → Register button
    • Delete an account → unregisterWithProxy + unpairClientWithProxy fire on the deleted account's pubkey

Stage 4

Final stage: AccountManager + PendingApprovalCoordinator (~700 LOC). Highest-risk because it touches the multi-account @Observable surface and the alert-chain state machine. After it lands, AppState.swift should be at the ~300-LOC slim target.

🤖 Generated with Claude Code

Stage 3c — last of the Stage 3 sub-extractions. Moves all proxy HTTP +
NIP-98 + retry-queue logic out of AppState.swift into a dedicated
extension file. The new file owns the [Pair] log category from PR #27.
Moves out of Clave/AppState.swift:
- registerWithProxy(completion:) — single-account wrapper
- registerAllAccountsWithProxy() — multi-account fan-out
- registerSignerWithProxy(signer:completion:) — private NIP-98 POST
/register impl
- ensureRegisteredFresh() — throttled scenePhase trigger
- ensureAllRegisteredFresh() — multi-account variant
- unregisterWithProxy(signer:) — NIP-98 POST /unregister
- pairClientWithProxy(clientPubkey:relayUrls:signer:) — POST /pair-client
with retry queue on failure
- unpairClientWithProxy(clientPubkey:signer:) — POST /unpair-client
- drainPendingPairOps() — retry queue worker
- retryPairOp(op:relayUrls:) — private retry impl
- retryUnpairOp(op:) — private retry impl
- "// MARK: - Proxy per-client-relay (V2)" header
- private let logger = Logger(...category: "pair") relocated from
AppState.swift line 8 (16/16 of its uses moved with the cluster)
Into the new file:
- Clave/AppState+ProxyClient.swift (577 LOC) — extension AppState
Logger relocation note: declaring Logger(subsystem:..., category:...)
in two files is equivalent — multiple Logger instances with the same
subsystem + category combine into one log stream. The dev-menu "Copy
Recent Logs" path (LogExporter.allCategories includes "pair") is
unchanged.
Zero behavior change:
- Function bodies preserved byte-for-byte.
- Original `private` modifiers preserved on registerSignerWithProxy,
retryPairOp, retryUnpairOp.
- All call sites (network code, Task { @mainactor in } closures,
URLSession callbacks, NIP-98 signing) unchanged.
- Cross-extension call from AppState+NostrConnect.swift's
handleNostrConnect to pairClientWithProxy still works — both are
extension AppState in the same module.
External callers unchanged:
- Clave/ClaveApp.swift (drainPendingPairOps via NotificationCenter)
- Clave/Views/MainTabView.swift (ensureRegisteredFresh +
ensureAllRegisteredFresh on scenePhase)
- Clave/Views/Settings/SettingsView.swift (registerWithProxy,
unregisterWithProxy)
- Clave/Views/Onboarding/OnboardingView.swift (registerWithProxy)
- Clave/Views/Home/HomeView.swift (registerAllAccountsWithProxy)
- Clave/Views/Home/ClientDetailView.swift (unpairClientWithProxy)
All call via appState.<method> — extension dispatch.
No pbxproj edits — Clave/ directory is auto-synced.
Verification:
- xcodebuild test -skip-testing:ClaveUITests on iPhone 17 / iOS 26.4:
- Pre-baseline (main @ 11c7117): 232 passed / 0 failed
- Post-extraction (this branch): 232 passed / 0 failed
- ** TEST SUCCEEDED ** in both runs
AppState.swift: 1,483 -> 922 LOC (-561, -37.8%).
Combined Stages 1+2+3a+3b+3c: 2,338 -> 922 LOC (-60.6% from original).
Stage 4 (AccountManager + PendingApprovalCoordinator, ~700 LOC) is
the final and highest-risk extraction — touches multi-account
@observable surface and the alert-chain state machine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DocNR
DocNR merged commit 7823f92 into mainMay 8, 2026
@DocNR
DocNR deleted the refactor/proxy-client-extension branch May 8, 2026 19:51
DocNR added a commit that referenced this pull request May 8, 2026
Internal-only build for on-device verification of [#39] (the final
Stage 3 sub-extraction; -561 LOC from AppState.swift, [Pair] logger
relocated). Bumps CURRENT_PROJECT_VERSION 67 -> 68 across all 4
targets in both Debug and Release configs.
Stage 3c verification gates (all in one TF cycle):
- xcodebuild test passes pre/post (232/232; no test count change) ✓
- Build 68 archives + uploads to TestFlight (this PR enables)
- Live network paths exercised on device:
- Cold launch -> ensureRegisteredFresh fires
- Pair a fresh client -> pairClientWithProxy registers URI relays
with proxy + emits [Pair] log breadcrumbs
- Sign one event end-to-end (proxy catches request -> APNs -> NSE
-> response published)
- Settings -> Register button (registerWithProxy)
- Delete an account -> unregisterWithProxy + unpairClientWithProxy
fire on the deleted account's pubkey
Stage 3 is now complete:
- Stage 1: RelayUtils.swift (pure helpers)
- Stage 2: legacy migration deletion
- Stage 3a: AppState+NostrConnect.swift
- Stage 3b: AppState+ProfileFetcher.swift
- Stage 3c: AppState+ProxyClient.swift (this)
AppState.swift: 2,338 -> 922 LOC (-60.6% from sprint start).
Stage 4 (AccountManager + PendingApprovalCoordinator, ~700 LOC) is
the final extraction.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@DocNR