Skip to main content

Module change_callbacks

Module change_callbacks 

Source
Expand description

Unstable: post-commit notification of group-state changes.

Registered once at client construction (crate::builder::ClientBuilder), not passed per call — the changes worth reacting to arrive from the stream and sync paths, where no SDK method call is on the stack to carry a parameter.

§Why a struct of callbacks

Only UnstableChangeCallbacks::app_data is implemented today. The registry is a struct rather than a bare callback argument so callbacks for the other mutable fields (name, description, image url, admin lists, permissions, disappearing settings) land as additive fields on a type the SDKs already construct — the same reason the bindings’ UpdateAppDataOptions and the other FFI options records are structs.

§Delivery contract

Callbacks fire once the storage transaction has committed, the group commit lock has been released, and the per-group sync mutex has been dropped — so an implementation is free to publish the result of its merge with update_app_data on the same group. That call re-enters sync_with_conn and takes the same sync mutex, which is why dispatch cannot happen inline during message processing; see crate::groups::MlsGroup::dispatch_app_data_changes.

Concretely, that means the sync path delivers a batch after the whole sync completes rather than between messages, and the stream path delivers each change as its message is processed. Changes are dispatched one at a time, in the order they were observed — but see Timeouts: that ordering holds only for callbacks that return within their budget.

A callback observes the net change across one processed message, and fires for local commits as well as remote ones — an implementation that reacts by writing must make its merge idempotent, or it will chase its own echo.

§Timeouts

Each callback is given UnstableChangeCallbacks::app_data_timeout to return. A host that overruns it is abandoned: the expiry is logged and the rest of that batch is dropped, and neither the sync nor the stream fails, because the change being reported is already durably committed and the callback is advisory. Nothing is permanently lost — merges are idempotent, so the next change re-triggers one from current state.

Abandoning is not cancelling. libxmtp drops the future and stops waiting; whether the host’s own work stops is up to the binding. uniffi notifies the foreign side that the future was dropped, which its Kotlin and Swift bindings can wire to cancelling the task, whereas a JS promise behind napi or wasm-bindgen keeps running to completion — or never resolves — with nothing left listening.

That has a consequence worth designing around: on a binding that cannot cancel, an abandoned callback may still publish after a later one already did, landing a merge derived from state that has since moved on. The budget bounds how long sync waits; it cannot unwind work the host has already started. Pass the compare-and-swap guardupdate_app_data(merged, Some(value_you_were_handed)) — so a late write is superseded at publish time instead of clobbering the newer one. Hosts that publish unguarded get last-writer-wins, and after a timeout “last” is not necessarily “latest”.

The budget also only bounds callbacks that yield. A handler that blocks its thread — synchronous FFI work, a blocking lock, Thread.sleep — stalls the task the timer lives on, so the timeout cannot fire and sync waits as long as the host does. This is inherent to async: a future that never returns from poll cannot be timed out from inside the same runtime. Callbacks must not block; do blocking work on the host’s own executor and await its completion.

§Stability

Pre-release. The shape of the payloads and of the registry may change without a major version bump until this graduates onto the stable client surface.

Structs§

AppDataChange
A change to a group’s opaque app_data slot, observed after it was applied to local state.
UnstableChangeCallbacks
The set of change callbacks registered on a client.

Constants§

DEFAULT_APP_DATA_CALLBACK_TIMEOUT
How long a single app_data callback may run before it is abandoned.

Traits§

AppDataChangeCallback
Notified whenever a processed message changed a group’s app_data.