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.
@Composablefun 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") } } } }}Reads: AsyncResource
Section titled “Reads: AsyncResource”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
datais 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.
Writes: AsyncMutation
Section titled “Writes: AsyncMutation”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.
Live state
Section titled “Live state”| 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 |