Skip to content

Commit 8b00bb4

Browse files
Add RPC tester interface to example app (#1085)
Adds a new button on the bottom control bar for the "rpc tester": <img width="100" height="103" alt="Screenshot 2026-05-15 at 11 09 19 AM" src="https://github.com/user-attachments/assets/5d7db331-f817-4321-9a22-4834d0966612" /> When clicked, it opens up a new panel which has two sections: <img width="508" height="855" alt="Screenshot 2026-05-15 at 11 09 04 AM" src="https://github.com/user-attachments/assets/0ea49592-18d3-49b9-a603-959a8d8de175" /> The top section allows sending RPC messages - select a destination participant, topic, and enter a payload, and click "Send". Note that there are two payload presets - `"hello world"` and `"X" * 20000` to provide some easy to generate data to test some edge cases. The bottom section lets you configure rpc handlers. Enter a rpc handler topic and a static response, and submit. Then when this participant receives a RPC request, the request will be responded to with the static response. This static response also has the same payload presets as the request. > [!WARNING] > This pull request was LLM generated and has only been lightly reviewed by a human. The author has tested this and confirms it works in the happy path, but no other validation has been done. > > A more thorough review of this needs to occur before it could be merged. --------- Co-authored-by: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com>
1 parent 8f86c55 commit 8b00bb4

2 files changed

Lines changed: 47 additions & 20 deletions

File tree

.changes/harden-rpc-tester

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="fixed" "Harden the example RPC tester"

example/lib/widgets/rpc_test_sheet.dart

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,13 @@ class RpcHandlerEntry {
2828
}
2929

3030
class RpcTestController extends ChangeNotifier {
31+
// Oldest invocations are dropped past this point so a chatty peer cannot
32+
// grow the log without bound.
33+
static const _maxInvocationsPerHandler = 200;
34+
3135
final List<RpcHandlerEntry> _handlers = [];
3236
Room? _room;
37+
bool _disposed = false;
3338

3439
List<RpcHandlerEntry> get handlers => List.unmodifiable(_handlers);
3540

@@ -60,21 +65,33 @@ class RpcTestController extends ChangeNotifier {
6065
}
6166

6267
final entry = RpcHandlerEntry(topic: topic, staticResponse: staticResponse);
68+
try {
69+
room.registerRpcMethod(topic, (data) async {
70+
final bytes = utf8.encode(data.payload).length;
71+
entry.invocations.insert(
72+
0,
73+
RpcInvocationRecord(
74+
timestamp: DateTime.now(),
75+
byteLength: bytes,
76+
payload: data.payload,
77+
callerIdentity: data.callerIdentity,
78+
),
79+
);
80+
if (entry.invocations.length > _maxInvocationsPerHandler) {
81+
entry.invocations.removeLast();
82+
}
83+
if (!_disposed) {
84+
notifyListeners();
85+
}
86+
return entry.staticResponse;
87+
});
88+
} on Exception {
89+
// The room throws when another component already registered this
90+
// method. Adding a card for it would let the tester unregister a
91+
// handler it does not own.
92+
return false;
93+
}
6394
_handlers.add(entry);
64-
room.registerRpcMethod(topic, (data) async {
65-
final bytes = utf8.encode(data.payload).length;
66-
entry.invocations.insert(
67-
0,
68-
RpcInvocationRecord(
69-
timestamp: DateTime.now(),
70-
byteLength: bytes,
71-
payload: data.payload,
72-
callerIdentity: data.callerIdentity,
73-
),
74-
);
75-
notifyListeners();
76-
return entry.staticResponse;
77-
});
7895
notifyListeners();
7996
return true;
8097
}
@@ -97,15 +114,14 @@ class RpcTestController extends ChangeNotifier {
97114
// is needed. Skip notifyListeners to avoid rebuild loops with the
98115
// editing TextField that drives this method.
99116
void updateStaticResponse(String topic, String response) {
100-
final entry = _handlers.firstWhere(
101-
(h) => h.topic == topic,
102-
orElse: () => throw StateError('topic $topic not registered'),
103-
);
117+
final entry = _handlers.firstWhere((h) => h.topic == topic);
104118
entry.staticResponse = response;
105119
}
106120

107121
@override
108122
void dispose() {
123+
// The flag keeps in flight RPC handlers from notifying after disposal.
124+
_disposed = true;
109125
_unregisterAll();
110126
_room = null;
111127
super.dispose();
@@ -193,8 +209,13 @@ class _RpcTestSheetState extends State<RpcTestSheet> {
193209
super.dispose();
194210
}
195211

212+
// The selected identity is only usable while that participant is still in
213+
// the room.
214+
String? get _validSelectedIdentity =>
215+
widget.room.remoteParticipants.values.any((p) => p.identity == _selectedIdentity) ? _selectedIdentity : null;
216+
196217
Future<void> _send() async {
197-
final identity = _selectedIdentity;
218+
final identity = _validSelectedIdentity;
198219
final method = _methodCtl.text.trim();
199220
final local = widget.room.localParticipant;
200221
if (identity == null || method.isEmpty || local == null) {
@@ -321,7 +342,12 @@ class _RpcTestSheetState extends State<RpcTestSheet> {
321342
final remotes = widget.room.remoteParticipants.values.toList();
322343
final identities = remotes.map((p) => p.identity).toList();
323344
final currentValue = identities.contains(_selectedIdentity) ? _selectedIdentity : null;
345+
// The form field only reads initialValue when its state is
346+
// created, so key it by the participant list. Otherwise a
347+
// selection can outlive its participant and trip the framework
348+
// assert that the value must be among the items.
324349
return DropdownButtonFormField<String>(
350+
key: ValueKey(identities.join(',')),
325351
initialValue: currentValue,
326352
decoration: const InputDecoration(
327353
labelText: 'Destination',
@@ -401,7 +427,7 @@ class _RpcTestSheetState extends State<RpcTestSheet> {
401427

402428
bool _canSend() =>
403429
!_isSending &&
404-
_selectedIdentity != null &&
430+
_validSelectedIdentity != null &&
405431
_methodCtl.text.trim().isNotEmpty &&
406432
widget.room.localParticipant != null;
407433

0 commit comments

Comments
 (0)