Skip to content

feat(ios-sdk): surface app data change callbacks and the update guard - #4015

Merged
tylerhawkes merged 1 commit into
mainfrom
tyler/appdata-cb-ios
Aug 20, 2026
Merged

tylerhawkes merged 1 commit into
mainfrom
tyler/appdata-cb-ios

Conversation

@tylerhawkes

@tylerhawkes tylerhawkes commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Stack

Merge bottom-up — each PR is based on its parent below.
Android is the priority path: #4019 → #4011 → #4012 → #4016.

Already merged: #4018 (proto regen). Related follow-up, independent of this stack: #4020.


Surfaces the unstable app-data change callback on the iOS SDK.

Public surface

ClientOptions(
    dbEncryptionKey: key,
    unstableChangeCallbacks: UnstableChangeCallbacks(appData: MyReconciler())
)

unstableChangeCallbacks defaults to nil, so this is source-compatible for every existing caller.

AppDataChangeHandler is an SDK-level protocol taking an AppDataChange with a hex groupId; FfiAppDataChangeHandlerBridge adapts it to the generated FFI protocol so FfiAppDataChange stays out of the public surface. UnstableChangeCallbacks.toFfi() returns nil when nothing is registered, so a client that does not use this never pays for the before/after snapshot on the message-processing path.

The identity-probe client in prepareClient passes nil explicitly — it never processes messages, so there is nothing to notify about.

About the xmtpv3.swift diff

sdks/ios/Sources/XMTPiOS/Libxmtp/xmtpv3.swift is generated by ./dev/bindings and checked in. Regenerating it to pick up the new types also pulled in ~680 lines of unrelated whitespace and formatting churn, because the committed copy was generated by an older uniffi-bindgen than the one the flake currently pins.

That churn is not hand-written. CI runs just ios test, which runs ./dev/bindings and regenerates this file from the same pinned flake, so it produces the same output — the committed copy was simply stale. Reviewers should read the FfiAppDataChange / FfiAppDataChangeCallback / FfiUnstableChangeCallbacks additions and the createClient signature, and can ignore the reformatting.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AdY7WKkNbJmdzpmUWvW1my

Note

Add app data change callbacks and compare-and-swap guard to iOS and Android SDKs

  • Adds UnstableChangeCallbacks to iOS and Android ClientOptions, allowing apps to register async handlers that fire with before/after values when a group's app_data field changes after a commit.
  • Extends Group.updateAppData on both platforms to accept an optional expectedAppData parameter, implementing compare-and-swap (CAS) semantics: the update is abandoned with a AppDataSuperseded error if the committed value has changed since the caller last read it.
  • In the core Rust MLS layer, adds UnstableChangeCallbacks, AppDataChangeCallback, and a new Superseded intent state; callbacks are dispatched after group locks are released to avoid deadlocks.
  • Wires the new changeCallbacks parameter into the createClient FFI function in the mobile bindings with a UniFFI default so existing call sites are unaffected.
  • Risk: AppDataSuperseded is classified as retryable, so guarded writes that lose a race will be retried automatically; callers should be aware of retry semantics when using the CAS guard.
📊 Macroscope summarized 15ced4b. 22 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@claude

claude Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Claude finished @tylerhawkes's task —— View job


Code Review Complete

Review Date: August 19, 2026, 4:40 PM UTC

This PR surfaces app data change callbacks and compare-and-swap guards on the iOS SDK. The implementation is well-structured and follows the existing patterns in the codebase.


Feedback

Code Quality and Best Practices ✅

Strong points:

  • Clean separation of concerns: UnstableChangeCallbacks.swift (sdks/ios/Sources/XMTPiOS/UnstableChangeCallbacks.swift:1) introduces a clear bridge pattern between SDK and FFI layers
  • The FfiAppDataChangeHandlerBridge adapter (sdks/ios/Sources/XMTPiOS/UnstableChangeCallbacks.swift:61) properly isolates FfiAppDataChange from the public API surface
  • Consistent naming and documentation following Swift conventions
  • Appropriate use of @_spi(Unstable) to gate pre-release APIs (though this callback is in stable ClientOptions)

Minor suggestions:

  • Consider documenting the threading/concurrency model more explicitly. The protocol is marked Sendable and uses async, but the implications of blocking message processing in onAppDataChanged could be clearer in the doc comments (sdks/ios/Sources/XMTPiOS/UnstableChangeCallbacks.swift:19-27)

Security Concerns ✅

No security issues identified:

  • No credentials or sensitive data exposure
  • Proper encapsulation of FFI types
  • The callback pattern doesn't introduce privilege escalation risks

Performance Considerations ⚠️

Concern: The doc comment states "The handler is awaited before message processing continues" (sdks/ios/Sources/XMTPiOS/UnstableChangeCallbacks.swift:21). This blocks message processing, which could impact throughput if handlers are slow. Consider:

  1. Documenting performance expectations for implementers
  2. Adding a note about timeout behavior if one exists
  3. If no timeout exists, consider whether one should be added to prevent indefinite hangs

Minor: The toFfi() method (sdks/ios/Sources/XMTPiOS/UnstableChangeCallbacks.swift:51) correctly returns nil when no handler is registered, avoiding the snapshot cost — good optimization.

API Design ✅

Good decisions:

  • expectedAppData parameter in updateAppData (sdks/ios/Sources/XMTPiOS/Group.swift:246-256) defaults to nil for last-writer-wins behavior while enabling compare-and-swap
  • UnstableChangeCallbacks is a struct with optional fields, allowing future additions without breaking existing callers
  • Doc comment clearly explains the compare-and-swap semantics

Question for consideration: Should there be an error case distinguishing "expectedValue mismatch" from other update failures? Callers reconciling state may want to handle that differently from network errors.

Test Coverage ⚠️

Gap identified: No tests found specifically exercising the new callback functionality. The existing GroupTests.swift:997 tests basic updateAppData, but:

  • No test verifies that AppDataChangeHandler.onAppDataChanged is invoked
  • No test validates the compare-and-swap behavior with expectedAppData
  • No test checks callback behavior for both local and remote changes
  • No test confirms idempotency requirements mentioned in the docs

Recommendation: Add integration tests covering:

  1. Callback invocation on local updates
  2. Callback invocation on remote updates
  3. Compare-and-swap guard preventing overwrites
  4. Handling of nil expected values

Generated Code

The ~680 line diff in xmtpv3.swift is uniffi-bindgen reformatting as the author noted. Spot-checking shows the substantive additions are FfiAppDataChange, FfiAppDataChangeCallback, and FfiUnstableChangeCallbacks types plus the createClient signature change — all expected.


Summary

This is a solid implementation with good API design. The main gaps are around test coverage (no tests for the new callback) and documentation of performance implications (blocking message processing). The compare-and-swap guard is a valuable addition for clients managing structured state.

Recommended actions before merge:

  1. Add integration tests for callback behavior and compare-and-swap semantics
  2. Clarify performance/timeout expectations in callback documentation

@macroscopeapp

macroscopeapp Bot commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Skipped

Macroscope did not run approvability analysis for this PR. Diff is too large for automated approval analysis, so this PR cannot be approved automatically.

Not approved because:

  • Macroscope's correctness review could not post its findings

You can add or adjust custom eligibility rules. Learn more.

@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-wasm-bindings branch from a92b803 to 5731d8d Compare August 19, 2026 16:48
@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-ios branch from a9e0194 to 7133b25 Compare August 19, 2026 16:48
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Aug 19, 2026
@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-ios branch from 7133b25 to 726f4a7 Compare August 19, 2026 17:20
@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-wasm-bindings branch from 5731d8d to 55cefed Compare August 19, 2026 17:20
@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-ios branch from 726f4a7 to b97f799 Compare August 19, 2026 17:51
@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-wasm-bindings branch from 55cefed to b364f55 Compare August 19, 2026 17:51
@macroscopeapp
macroscopeapp Bot dismissed their stale review August 19, 2026 17:51

Dismissing prior approval to re-evaluate b97f799

@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-wasm-bindings branch from b364f55 to 69ad642 Compare August 19, 2026 18:26
@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-ios branch from b97f799 to 2c3c282 Compare August 19, 2026 18:26
@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-wasm-bindings branch from 69ad642 to a8f796e Compare August 19, 2026 18:51
@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-ios branch from 2c3c282 to 89013da Compare August 19, 2026 18:51
@tylerhawkes
tylerhawkes changed the base branch from tyler/appdata-cb-wasm-bindings to tyler/appdata-cb-mobile August 19, 2026 18:51
@tylerhawkes tylerhawkes changed the title feat(ios-sdk): surface app data change callbacks on ClientOptions feat(ios-sdk): surface app data change callbacks and the update guard Aug 19, 2026
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (tyler/appdata-cb-mobile@f7a9599). Learn more about missing BASE report.

Additional details and impacted files
@@                    Coverage Diff                     @@
##             tyler/appdata-cb-mobile    #4015   +/-   ##
==========================================================
  Coverage                           ?   85.92%           
==========================================================
  Files                              ?      419           
  Lines                              ?    68292           
  Branches                           ?        0           
==========================================================
  Hits                               ?    58681           
  Misses                             ?     9611           
  Partials                           ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Base automatically changed from tyler/appdata-cb-mobile to tyler/appdata-cb-core August 19, 2026 20:41
@tylerhawkes
tylerhawkes force-pushed the tyler/appdata-cb-ios branch from ee56410 to 15ced4b Compare August 19, 2026 20:47
@tylerhawkes
tylerhawkes changed the base branch from tyler/appdata-cb-core to main August 19, 2026 20:47
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Aug 19, 2026
@macroscopeapp
macroscopeapp Bot dismissed their stale review August 19, 2026 20:51

Dismissing prior approval to re-evaluate 15ced4b

@tylerhawkes tylerhawkes reopened this Aug 20, 2026
@tylerhawkes
tylerhawkes merged commit fdb9297 into main Aug 20, 2026
34 of 35 checks passed
@tylerhawkes
tylerhawkes deleted the tyler/appdata-cb-ios branch August 20, 2026 14:55
Sign up for free to 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.

2 participants