fix(#88): cap the BLE APP_START handshake retry with backoff + keep-alive
The BLE/USB self-info retry was an unbounded 3.5s hammer -- the Web path caps at
3 and skips while syncing; BLE had neither. On a slow device handshake it churned
the connect and starved the contact sync, hanging the contact-load bar. Inherited
from upstream zjs81/meshcore-open (git blame; zero Offband commits).
- Replace the unbounded 3.5s Timer.periodic with a self-rescheduling backoff:
3.5s -> 7 -> 14 -> 28 -> 56s, then a steady ~60s keep-alive. Never hard-stops --
a slow/recovering device still gets prompted.
- Add the Web path's skip-while-syncing guard on BLE (per-tick skip, never
permanent: the timer keeps rescheduling, so a settled sync lets the next tick
send).
- Decisions extracted as pure, tested helpers: nextAppStartRetryDelay(attempt)
and shouldSendAppStartRetry({connected, awaitingSelfInfo, syncing}).
- Disconnect teardown (Gemini's amendment) was already present (:2501 / :2374).
Gemini plan-review: Proceed, no blockers. 6 new helper tests; full suite 299 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/99/head
parent
2ac0755361
commit
3015b41f55
@ -0,0 +1,95 @@
|
||||
// APP_START handshake-retry policy helpers (#88).
|
||||
//
|
||||
// The BLE/USB handshake retry was an unbounded 3.5s hammer; on a slow device it
|
||||
// churned the connect and starved the contact sync (stuck red bar). These pin
|
||||
// the replacement policy: an exponential backoff that settles into a slow
|
||||
// keep-alive (never a hard stop), and a send-gate that skips while an initial
|
||||
// sync is in flight (mirroring the Web path).
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
|
||||
void main() {
|
||||
group('nextAppStartRetryDelay', () {
|
||||
test('ramps 3.5 -> 7 -> 14 -> 28 -> 56s then holds at 60s', () {
|
||||
expect(
|
||||
MeshCoreConnector.nextAppStartRetryDelay(0),
|
||||
const Duration(milliseconds: 3500),
|
||||
);
|
||||
expect(
|
||||
MeshCoreConnector.nextAppStartRetryDelay(1),
|
||||
const Duration(seconds: 7),
|
||||
);
|
||||
expect(
|
||||
MeshCoreConnector.nextAppStartRetryDelay(2),
|
||||
const Duration(seconds: 14),
|
||||
);
|
||||
expect(
|
||||
MeshCoreConnector.nextAppStartRetryDelay(3),
|
||||
const Duration(seconds: 28),
|
||||
);
|
||||
expect(
|
||||
MeshCoreConnector.nextAppStartRetryDelay(4),
|
||||
const Duration(seconds: 56),
|
||||
);
|
||||
expect(
|
||||
MeshCoreConnector.nextAppStartRetryDelay(5),
|
||||
const Duration(seconds: 60),
|
||||
);
|
||||
});
|
||||
|
||||
test('never hammers (>= 3.5s) and never exceeds the 60s keep-alive', () {
|
||||
for (var attempt = 0; attempt < 64; attempt++) {
|
||||
final d = MeshCoreConnector.nextAppStartRetryDelay(attempt);
|
||||
expect(d.inMilliseconds, greaterThanOrEqualTo(3500));
|
||||
expect(d.inMilliseconds, lessThanOrEqualTo(60000));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('shouldSendAppStartRetry', () {
|
||||
test('sends only while connected, awaiting self-info, and not syncing', () {
|
||||
expect(
|
||||
MeshCoreConnector.shouldSendAppStartRetry(
|
||||
connected: true,
|
||||
awaitingSelfInfo: true,
|
||||
syncing: false,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not send when disconnected', () {
|
||||
expect(
|
||||
MeshCoreConnector.shouldSendAppStartRetry(
|
||||
connected: false,
|
||||
awaitingSelfInfo: true,
|
||||
syncing: false,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not send once the handshake is no longer awaited', () {
|
||||
expect(
|
||||
MeshCoreConnector.shouldSendAppStartRetry(
|
||||
connected: true,
|
||||
awaitingSelfInfo: false,
|
||||
syncing: false,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('skips while an initial sync is in flight', () {
|
||||
expect(
|
||||
MeshCoreConnector.shouldSendAppStartRetry(
|
||||
connected: true,
|
||||
awaitingSelfInfo: true,
|
||||
syncing: true,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in new issue