perf(mobile): ask typing subscriptions only for the typing window (#9454)

Every channel that shows a typing indicator (the open channel, and each
Solo row on the home list) subscribes for typing and message events with
`limit: 10` and no `since`, so each subscribe replays that channel's
last 10 stored messages. The handler then ignores every one of them that
is older than the 8 second typing window, so the replay only costs
bytes. On an iPhone 12 mini signed in to Builderlab, seven Solo rows
made this about 66 KB of kind 9 messages on each cold start, and again
on each resubscribe.

The subscription now asks only for events inside the typing window
(`since` = now minus 8 s), which are the only ones the handler can act
on.

### Validation

New test: the typing subscription asks for history starting 8 s before
it subscribes. It fails without this change.

Full mobile test suite and `flutter analyze` pass.

On the iPhone 12 mini (profile build with this change, signed in to
Builderlab), stored and live events on the typing subscriptions in the
first minute or more after a cold start:

| | Subscriptions | Kind 9 messages | Typing events |
| --- | --- | --- | --- |
| Before (60 s) | 7 | 74 (66.6 KB) | 26 (15.6 KB) |
| After (80 s) | 8 | 2 (1.5 KB) | 29 (17.8 KB) |

Typing events are mostly live ones from agents working in Solo chats, so
they stay about the same. The automated run covered a cold start to the
home list and its Solo rows.

### Regressions

- A device clock running more than 8 s fast can make the relay skip a
message that would have ended an indicator. That indicator still clears
when its 8 s window ends, and the handler already drops events it
considers older than the window, so the same skew loses the same events
today.

Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
This commit is contained in:
Tom Brow
2026-11-13 14:40:17 -08:00
committed by GitHub
parent 11ea8c928e
commit 3764a2e4d5
2 changed files with 20 additions and 0 deletions

View File

@@ -187,6 +187,9 @@ class ChannelTypingNotifier extends Notifier<List<TypingEntry>> {
tags: {
'#h': [channelId],
},
// Typing is ephemeral, and stored messages only matter while they
// can still end a visible indicator (see [_handleTypingEvent]).
since: DateTime.now().millisecondsSinceEpoch ~/ 1000 - _ttlMs ~/ 1000,
limit: 10,
),
(event) {

View File

@@ -9,6 +9,7 @@ import '../../helpers/thread_ids.dart';
class _Relay extends RelaySessionNotifier {
late void Function(NostrEvent) receive;
int calls = 0;
NostrFilter? filter;
bool reject = false;
void Function(String)? close;
int active = 0;
@@ -24,6 +25,7 @@ class _Relay extends RelaySessionNotifier {
}) async {
expect(filter.kinds, containsAll([20002, 9, 40002, 40008]));
calls++;
this.filter = filter;
close = onClosed;
if (reject) throw StateError('rejected');
receive = onEvent;
@@ -43,6 +45,21 @@ void _settle(ProviderContainer container, FakeAsync async) {
}
void main() {
test('asks only for stored history inside the typing window', () async {
final relay = _Relay();
final container = ProviderContainer(
overrides: [relaySessionProvider.overrideWith(() => relay)],
);
addTearDown(container.dispose);
final before = DateTime.now().millisecondsSinceEpoch ~/ 1000;
container.listen(channelTypingProvider('chat'), (_, _) {});
await Future<void>.delayed(Duration.zero);
final after = DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Older messages can no longer end an indicator, so replaying them on
// every subscribe only downloads them.
expect(relay.filter!.since, inInclusiveRange(before - 8, after - 8));
});
test(
'initial rejection retries with a bound and no uncaught error',
() => fakeAsync((async) {