Skip to content

Compose State Holders

Every mero-react hook has a state holder in com.calimero.mero.compose.state: a plain Kotlin class over MeroClient.session, unit-testable without a device, and a thin remember* composable that keeps one per input and closes it when it leaves the composition.

@Composable
fun Members(groupId: String) {
val members by rememberGroupMembers(groupId).state.collectAsStateWithLifecycle()
val remove = rememberRemoveGroupMembers()
val scope = rememberCoroutineScope()
when {
members.loading -> CircularProgressIndicator()
members.error != null -> Text("Couldn't load: ${members.error?.message}")
else -> members.data.forEach { m ->
Row {
Text(m.identity)
Button(onClick = { scope.launch { remove.removeGroupMembers(groupId, RemoveGroupMembersRequest(listOf(m.identity))) } }) {
Text("Remove")
}
}
}
}
}

AsyncResource<T> is mero-react’s useAsyncResource: state is a StateFlow<ResourceState<T>> (data, loading, error), and refetch() reads again.

  • Latest request wins. A new input or a new session invalidates any request in flight, so a slow earlier answer can never overwrite a newer one.
  • Reset on input change. A new input is a new holder (the remember* key), which starts from the initial value.
  • Session-bound. Every read re-runs when the session changes — a sign-in, a sign-out, a relay learned — as a hook re-runs on a new admin.
  • A null input is a disabled read: nothing is fetched and data is the initial value.

MeroReads(sessions, scope) builds them: contexts(applicationId) / applicationContexts, contextGroup, groupInfo, groupMembers (keyed by account), groupContexts, subgroups, defaultCapabilities, subgroupVisibility, groupUpgradeStatus, groupMetadata, memberMetadata, namespaces, namespace, nodeIdentity, namespacesForApplication, namespaceGroups and blobInfo (isBlobNotFound(error) tells “no holder” from a failure). On an account, contexts and namespacesForApplication list the account’s own across every relay it uses and filter them, since the per-application listings are node-wide.

Each write hook is a class with one method of the same name, tracking loading, error and failure (a ClassifiedError). It resolves null with no session, or on failure — unless built with throwOnError = true, which rethrows the original error.

CreateContext, CreatePrivateContext (a restricted group, its members, then the context — through warrants on an account), DeleteContext, JoinContext, ResyncContext, DeleteGroup, SyncGroup, AddGroupMembers (takes the signing key), RemoveGroupMembers, UpdateMemberRole, SetDefaultCapabilities, SetSubgroupVisibility, SetTeeAdmissionPolicy, ReparentGroup, DetachContextFromGroup, JoinSubgroupInheritance, GroupInvitations, JoinGroup, SetGroupMetadata, SetMemberMetadata, SetContextMetadata, CreateNamespace, DeleteNamespace, CreateNamespaceInvitation, JoinNamespace, CreateGroupInNamespace, RedeemInvitation (keeps its RedeemOutcome), UpgradeGroup, RetryGroupUpgrade, InstallFromRegistry and UploadBlob — each with a remember…() composable.

Holder mero-react hook What it does
ExecuteState(contextId) useExecute execute(method, args) through the session’s transport; an account with no relay is told an invitation is the cure
SubscriptionState(contextIds, groupIds, onEvent) useSubscription connects, subscribes, hands over those ids’ events
EphemeralState(contextId, codec, initial, throttleMs, includeSelf) useEphemeral peers, setPresence { it.copy(…) } (trailing-edge throttle, 30 ms), ageOf(author), error; replays are authoritative and your own echo is filtered
ContextDiscoveryState(options) useContextDiscovery waits for a new context of an application (alias first)
GroupCapabilitiesState(groupId, memberId) useGroupCapabilities read and set a member’s mask
MigrationEventsState(namespaceId, onEvent) useMigrationEvents the namespace’s GroupMigration frames, and live
MigrationStatusState(namespaceId, pollIntervalMs) useMigrationStatus the rollup, re-read on each frame, polling every 5 s while the stream is down
AppVersionState(contextId, expected) useAppVersion appVersion, isStale, live from AppVersionChanged
LatestVersionState(registryUrl, packageName, currentVersion) useLatestVersion latestVersion, updateAvailable
GroupAppVersionState(groupId) useGroupAppVersion the version a group runs and whether a downloaded update is not yet applied
MyAuthoredMigrationState(contextId) useMyAuthoredMigration pendingCount, authorize()
BlobFileState(blobId, cacheDir, contextId) useBlobUrl the blob as a cache file, deleted when replaced or closed