feat(android-sdk): surface app data change callbacks and the update guard - #4016
Conversation
|
Claude finished @tylerhawkes's task —— View job PR Review — 2026-08-19 20:04 UTCSummaryThis PR successfully surfaces the app-data change callback and compare-and-swap guard to the Android SDK. The implementation is clean, follows Kotlin best practices, and maintains source compatibility. Below are observations on code quality, potential issues, and testing. Code Quality & Best Practices✅ Strengths:
Potential Bugs & Edge CasesThe // Thread A reads appData = "v1"
val current = group.appData() // "v1"
// Thread B updates to "v2" and commits
group.updateAppData("v2")
// Thread A tries to update with guard, expecting "v1"
group.updateAppData("new-value", expectedAppData = current) // Will fail correctlyThis is correct behavior — the guard works as intended. However, the documentation in Group.kt:618-627 could be clearer about retry semantics. When the guard fails, should callers:
Recommendation: Add a KDoc example showing the retry pattern, similar to this: suspend fun mergeAppData(group: Group, newData: String) {
var attempts = 0
while (attempts < MAX_RETRIES) {
val current = group.appData()
val merged = merge(current, newData)
try {
group.updateAppData(merged, expectedAppData = current)
return // Success
} catch (e: XMTPException) {
if (e.message?.contains("Permission denied") == true) throw e
attempts++
// Guard failed, retry the merge
}
}
throw XMTPException("Failed to update appData after $MAX_RETRIES attempts")
}Security Concerns✅ No major security issues identified.
Minor observation: The Performance Considerations✅ Generally efficient:
The FFI callback is awaited during message processing (per the Rust doc in change_callbacks.rs:5-6). If a handler implementation is slow (e.g., complex merge logic, database writes), it will block message processing for that group. Recommendation: Document this in the KDoc for /**
* Notified when a processed message changed a group's `appData`.
*
* [onAppDataChanged] is awaited before message processing continues, so a
* semantic merge — including republishing via `Group.updateAppData` — can
* finish first. It fires for changes this client made as well as remote ones,
* so the merge must be idempotent.
*
* **Performance note:** This callback blocks message processing for the group
* until it completes. Keep implementations fast or offload heavy work to a
* background queue.
*/Test Coverage❌ No tests found for the new functionality:
Recommendation: Add tests to @Test
fun testAppDataChangeCallback() = runBlocking {
val changes = mutableListOf<AppDataChange>()
val handler = object : AppDataChangeHandler {
override suspend fun onAppDataChanged(change: AppDataChange) {
changes.add(change)
}
}
val client = Client.create(
account = fixtures.aliceAccount,
options = fixtures.clientOptions.copy(
unstableChangeCallbacks = UnstableChangeCallbacks(appData = handler)
)
)
val group = client.conversations.newGroup(listOf(fixtures.bob.walletAddress))
group.updateAppData("test-data")
group.sync()
assertEquals(1, changes.size)
assertEquals(group.id, changes[0].groupId)
assertNull(changes[0].oldValue)
assertEquals("test-data", changes[0].newValue)
}
@Test
fun testAppDataUpdateGuard() = runBlocking {
val group = fixtures.aliceClient.conversations.newGroup(listOf(fixtures.bob.walletAddress))
group.updateAppData("v1")
group.sync()
// This should succeed
group.updateAppData("v2", expectedAppData = "v1")
group.sync()
// This should fail (expected value doesn't match)
assertFailsWith<XMTPException> {
group.updateAppData("v3", expectedAppData = "v1")
}
}Final VerdictApprove with recommendations: The code is well-structured, safe, and follows Android SDK conventions. The main gaps are:
|
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR introduces a new feature (app data change callbacks) with new interfaces, callback mechanisms, and compare-and-swap semantics. While additive and backwards-compatible, new user-facing capabilities and FFI integrations warrant human review. No code changes detected at You can add or adjust custom eligibility rules. Learn more. |
d4dbbd2 to
82a39d3
Compare
82a39d3 to
4feed06
Compare
fa75814 to
d18cdf6
Compare
d18cdf6 to
ae43c0d
Compare
ae43c0d to
ca3e275
Compare
ca3e275 to
c072dae
Compare
Stack
Merge bottom-up — each PR is based on its parent below.
Android is the priority path: #4019 → #4011 → #4012 → #4016.
IntentState::Superseded(base:main)Already merged: #4018 (proto regen). Related follow-up, independent of this stack: #4020.
Surfaces the unstable app-data change callback on the Android SDK.
Public surface
unstableChangeCallbacksdefaults tonull, so this is source-compatible for every existing caller.AppDataChangeHandleris an SDK-level interface with asuspend funtaking anAppDataChangewith a hexgroupId; a private bridge adapts it to the generatedFfiAppDataChangeCallbacksoFfiAppDataChangestays out of the public surface.toFfi()returnsnullwhen 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 passes
nullexplicitly — it never processes messages, so there is nothing to notify about.🤖 Generated with Claude Code
https://claude.ai/code/session_01AdY7WKkNbJmdzpmUWvW1my
Note
Surface app data change callbacks and update guard in Android SDK
UnstableChangeCallbackswithAppDataChangeHandlerinterface and FFI bridge, allowing callers to receive notifications when a group'sappDatachanges.ClientOptionswith an optionalunstableChangeCallbacksfield; callbacks are forwarded to the FFI client on construction for both in-memory and persistent DB clients (identity-probe client explicitly skips them).expectedAppDataparameter toGroup.updateAppDataimplementing a compare-and-swap guard — the core rejects the update if the committed value differs from the expected value.Macroscope summarized c072dae.