If SQLite clearAll fails mid-panic, in-memory state was already cleared
but a process restart could reload encrypted history from disk (#699).
Fall back to deleting the database files before reporting failure.
A relay that fails is retried on an exponential backoff, then abandoned
for the lifetime of the process. Nothing brings it back: the relay layer
registers no connectivity callback, the periodic subscription validator
only repairs subscriptions on sockets that are already open (and returns
immediately when connectedRelayCount is 0, which is exactly the state
after an outage), and connect() runs once from NostrClient.initialize().
The remaining paths that reset reconnectAttempts are a manual retry, a
Tor state change, and a successful open.
Two ways a relay died permanently:
- Any error whose message mentioned DNS returned before scheduling
anything at all. "Unable to resolve host" is what this device reports
when it simply has no network, so a moment in a tunnel killed every
relay at once, with no retry ever.
- Otherwise the schedule stopped at MAX_RECONNECT_ATTEMPTS. With
INITIAL=1s and MULTIPLIER=2 that is nine waits totalling about eight
and a half minutes, after which the relay was dead. MAX_BACKOFF_INTERVAL
was unreachable: attempt 9 asks for 256s and attempt 10 gave up, so the
five-minute ceiling the constant defines never applied to anything.
Let the backoff saturate at MAX_BACKOFF_INTERVAL and keep retrying there.
A name-resolution failure now backs off like any other error. Steady state
costs one connection attempt per relay per five minutes; the previous
behaviour cost the user every internet DM, delivery receipt and geohash
channel until they noticed and restarted the app.
The schedule moves into RelayReconnectPolicy so it is unit-testable
without OkHttp or a Context.
A dead drop can cross its NIP-40 expiry while it is on screen. Filtering
at ingest alone kept it visible until the subscription was recreated, so
the note outlived its expiry exactly in the session where someone is
reading it.
Notes now carry their expiry, and a 60s job started with the
subscription (and cancelled with it) drops the ones that have passed --
the same interval and behaviour as the iOS manager. Ids stay in noteIDs
so a relay replay cannot resurrect a dropped note.
NostrRelayManager runs every incoming event through NostrFilter.matches
before any handler sees it, and that comparison was exact. So the
case-insensitive handling in the previous commit was unreachable over the
relay path: a note tagged ["G", "U4PRUYD"] was dropped at the gate.
iOS has no such client-side filter and lowercases the tag name and
geohash where it reads them, so the note is visible there. The values
this app filters on are hex ids or geohashes, both of which encode the
same value in either case.
Two ways the Android note handler disagrees with the iOS one, on the same
event:
- NIP-40. Relays are not required to drop expired events, so iOS checks
the `expiration` tag client-side "so 24h dead drops actually vanish".
Android had no such check, so an expired note kept showing until the
list was cleared - the opposite of what a dead drop promises.
- Case. Tag names and geohashes are case-insensitive, and iOS lowercases
both before matching. Android compared them exactly, so a note tagged
["G", "U4PRUYD"] was visible on iOS and invisible on Android.
Both now follow the iOS handler.