mirror of
https://github.com/block/buzz.git
synced 2026-11-06 01:11:33 +00:00
fix(mobile): keep record agents in a Huddle while disconnected
The managed-agent record read returned an empty set while the relay was disconnected. That counted as a successful read, so a reconnect replaced the trusted records with nothing. An agent known only by its record then left the Huddle participant list and lost its "preparing a response" state until the next connected read. The read now fails while disconnected, so the last records loaded stay in use, still limited to current members. Each result carries the relay and account that read it, and a result from another account is not used. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -30,16 +30,21 @@ const managedAgentRecordKind = 30177;
|
||||
///
|
||||
/// The result holds only current members. Until the records first load,
|
||||
/// only the `bot` role counts. While the records reload after a membership
|
||||
/// change, or if that reload fails, the last loaded records still apply to
|
||||
/// the members who remain; a failed read never removes a `bot` agent.
|
||||
/// change, if that reload fails, or while the relay is disconnected, the last
|
||||
/// records loaded for the same relay and account still apply to the members
|
||||
/// who remain; a failed read never removes a `bot` agent.
|
||||
final huddleAgentPubkeysProvider = Provider.autoDispose
|
||||
.family<Set<String>, String>((ref, channelId) {
|
||||
final members =
|
||||
ref.watch(channelMembersProvider(channelId)).value ??
|
||||
const <ChannelMember>[];
|
||||
final recordAgents =
|
||||
ref.watch(huddleManagedAgentRecordPubkeysProvider(channelId)).value ??
|
||||
const <String>{};
|
||||
final scope = ref.watch(huddleManagedAgentRecordScopeProvider);
|
||||
final loaded = ref
|
||||
.watch(huddleManagedAgentRecordPubkeysProvider(channelId))
|
||||
.value;
|
||||
final recordAgents = loaded != null && loaded.scope == scope
|
||||
? loaded.agents
|
||||
: const <String>{};
|
||||
return Set.unmodifiable({
|
||||
for (final member in members)
|
||||
if (member.isBot ||
|
||||
@@ -48,25 +53,50 @@ final huddleAgentPubkeysProvider = Provider.autoDispose
|
||||
});
|
||||
});
|
||||
|
||||
/// The relay and account that a managed-agent record read belongs to.
|
||||
typedef HuddleManagedAgentRecordScope = ({String relay, String? viewer});
|
||||
|
||||
/// Agents named by trusted managed-agent records, with the relay and account
|
||||
/// that read them.
|
||||
typedef HuddleManagedAgentRecords = ({
|
||||
HuddleManagedAgentRecordScope scope,
|
||||
Set<String> agents,
|
||||
});
|
||||
|
||||
final huddleManagedAgentRecordScopeProvider =
|
||||
Provider<HuddleManagedAgentRecordScope>(
|
||||
(ref) => (
|
||||
relay: ref.watch(
|
||||
relayConfigProvider.select((config) => config.baseUrl),
|
||||
),
|
||||
viewer: ref.watch(myPubkeyProvider),
|
||||
),
|
||||
);
|
||||
|
||||
/// Fails, rather than returning no records, while the relay is disconnected:
|
||||
/// "not read" must not replace the last records read.
|
||||
final huddleManagedAgentRecordPubkeysProvider = FutureProvider.autoDispose
|
||||
.family<Set<String>, String>((ref, channelId) async {
|
||||
.family<HuddleManagedAgentRecords, String>((ref, channelId) async {
|
||||
final connected = ref.watch(
|
||||
relaySessionProvider.select(
|
||||
(state) => state.status == SessionStatus.connected,
|
||||
),
|
||||
);
|
||||
final viewer = ref.watch(myPubkeyProvider);
|
||||
final scope = ref.watch(huddleManagedAgentRecordScopeProvider);
|
||||
final members = await ref.watch(channelMembersProvider(channelId).future);
|
||||
if (!connected) return const {};
|
||||
if (!connected) {
|
||||
throw StateError('relay disconnected; managed-agent records not read');
|
||||
}
|
||||
try {
|
||||
return await fetchTrustedManagedAgentPubkeys(
|
||||
final agents = await fetchTrustedManagedAgentPubkeys(
|
||||
ref.read(relaySessionProvider.notifier),
|
||||
members: [
|
||||
for (final member in members)
|
||||
MemberEntry(pubkey: member.pubkey, role: member.role),
|
||||
],
|
||||
viewerPubkey: viewer,
|
||||
viewerPubkey: scope.viewer,
|
||||
);
|
||||
return (scope: scope, agents: agents);
|
||||
} catch (error) {
|
||||
debugPrint('[HuddleAgents] managed-agent query failed: $error');
|
||||
rethrow;
|
||||
|
||||
@@ -395,9 +395,12 @@ Widget _buildTestable({
|
||||
if (huddleMembersNotifier != null)
|
||||
_mutableHuddleMembersProvider.overrideWith(() => huddleMembersNotifier),
|
||||
if (huddleRecordAgentPubkeys != null)
|
||||
huddleManagedAgentRecordPubkeysProvider(
|
||||
_huddleChannelId,
|
||||
).overrideWith((ref) async => huddleRecordAgentPubkeys),
|
||||
huddleManagedAgentRecordPubkeysProvider(_huddleChannelId).overrideWith(
|
||||
(ref) async => (
|
||||
scope: ref.watch(huddleManagedAgentRecordScopeProvider),
|
||||
agents: huddleRecordAgentPubkeys,
|
||||
),
|
||||
),
|
||||
if (!watchChannelMembershipUpdates)
|
||||
channelBotPubkeysProvider(_channelId).overrideWith(
|
||||
(ref) async => loadChannelBotPubkeys?.call() ?? const <String>{},
|
||||
|
||||
@@ -95,6 +95,64 @@ void main() {
|
||||
expect(await _settledAgents(container), {_botAgent});
|
||||
});
|
||||
|
||||
test(
|
||||
'a relay disconnect keeps record agents until a connected read',
|
||||
() async {
|
||||
final relay = _HuddleRelaySession(
|
||||
members: {
|
||||
_owner.public: 'owner',
|
||||
_botAgent: 'bot',
|
||||
_ownerAgent: 'member',
|
||||
_viewerAgent: 'member',
|
||||
},
|
||||
records: [_record(_owner, _ownerAgent), _record(_owner, _viewerAgent)],
|
||||
);
|
||||
final container = _container(relay);
|
||||
expect(await _settledAgents(container), {
|
||||
_botAgent,
|
||||
_ownerAgent,
|
||||
_viewerAgent,
|
||||
});
|
||||
|
||||
// While disconnected, the last records still apply.
|
||||
relay.setStatus(SessionStatus.reconnecting);
|
||||
expect(await _settledAgents(container), {
|
||||
_botAgent,
|
||||
_ownerAgent,
|
||||
_viewerAgent,
|
||||
});
|
||||
expect(relay.recordFilters, hasLength(1));
|
||||
|
||||
// After reconnecting, a successful read is authoritative again.
|
||||
relay.records = [_record(_owner, _ownerAgent)];
|
||||
relay.setStatus(SessionStatus.connected);
|
||||
expect(await _settledAgents(container), {_botAgent, _ownerAgent});
|
||||
expect(relay.recordFilters, hasLength(2));
|
||||
},
|
||||
);
|
||||
|
||||
test('records read for another account do not apply', () async {
|
||||
final relay = _HuddleRelaySession(
|
||||
members: {_owner.public: 'owner', _viewerAgent: 'member'},
|
||||
// Only the viewer vouches for this agent.
|
||||
records: [_record(_viewer, _viewerAgent)],
|
||||
);
|
||||
final container = _container(relay);
|
||||
final viewer = container.read(_viewerOverride.notifier);
|
||||
// Load the Huddle once as the other account, so its member snapshot is
|
||||
// cached, then as the viewer.
|
||||
viewer.set(_stranger.public);
|
||||
expect(await _settledAgents(container), isEmpty);
|
||||
viewer.set(_viewer.public);
|
||||
expect(await _settledAgents(container), {_viewerAgent});
|
||||
|
||||
// Switching account while disconnected must not carry the viewer's
|
||||
// records over to the other account's cached members.
|
||||
relay.setStatus(SessionStatus.reconnecting);
|
||||
viewer.set(_stranger.public);
|
||||
expect(await _settledAgents(container), isEmpty);
|
||||
});
|
||||
|
||||
test('last-human count leaves out agents known by their record', () async {
|
||||
final relay = _HuddleRelaySession(
|
||||
members: {
|
||||
@@ -174,12 +232,21 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
final _viewerOverride = NotifierProvider<_Viewer, String?>(_Viewer.new);
|
||||
|
||||
class _Viewer extends Notifier<String?> {
|
||||
@override
|
||||
String? build() => _viewer.public;
|
||||
|
||||
void set(String? pubkey) => state = pubkey;
|
||||
}
|
||||
|
||||
ProviderContainer _container(_HuddleRelaySession relay) {
|
||||
final container = ProviderContainer(
|
||||
retry: (_, _) => null,
|
||||
overrides: [
|
||||
relaySessionProvider.overrideWith(() => relay),
|
||||
myPubkeyProvider.overrideWithValue(_viewer.public),
|
||||
myPubkeyProvider.overrideWith((ref) => ref.watch(_viewerOverride)),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
@@ -220,7 +287,7 @@ MemberEntry _member(String pubkey, String role) =>
|
||||
|
||||
class _HuddleRelaySession extends RelaySessionNotifier {
|
||||
Map<String, String> members;
|
||||
final List<NostrEvent> records;
|
||||
List<NostrEvent> records;
|
||||
Object? recordError;
|
||||
Completer<void>? recordGate;
|
||||
final List<NostrFilter> recordFilters = [];
|
||||
@@ -234,6 +301,8 @@ class _HuddleRelaySession extends RelaySessionNotifier {
|
||||
@override
|
||||
SessionState build() => const SessionState(status: SessionStatus.connected);
|
||||
|
||||
void setStatus(SessionStatus status) => state = SessionState(status: status);
|
||||
|
||||
@override
|
||||
Future<List<NostrEvent>> fetchHistory(
|
||||
NostrFilter filter, {
|
||||
|
||||
Reference in New Issue
Block a user