Document version 2026-05-29 · Context: 1300 ble_token_renew from a single guest during 4 hours of the owner being offline, which prompted a hunt for other sources of redundant HTTP and WebSocket traffic.
ble_token_renew in pending_host_msgs (a UNIQUE INDEX on host_id, dedup_key). The migration has been applied.GuestConn.requestBleTokenRenew — no more than once every 60 s per bleId.onOpen; it waits for a stable session of at least 60 s, which guards against a connect–drop–connect storm.Files: HostService.kt:1424-1432, Vault.kt:213
private val pendingKeysRunnable = object : Runnable {
override fun run() {
if (Prefs.isHostRegistered && Prefs.getPendingHostKeys().isNotEmpty()) {
Thread { syncPendingHostKeys() }.start()
}
pendingKeysHandler.postDelayed(this, 60_000L)
}
}
syncPendingHostKeys takes ALL pending host keys and calls Api.keyCreatefor each. If the HTTP call fails, the key stays pending, and 60 s later the cycle repeats.
В Prefs store per localId:
attempt_at_<localId> — the timestamp of the last attemptattempt_count_<localId> — the attempt counterOn each run of syncPendingHostKeys:
val waitMs = (30_000L * (1L shl attemptCount)).coerceAtMost(30 * 60_000L)
if (now - attemptAt < waitMs) return@forEach // skip
// otherwise try
val ok = try { Api.keyCreate(...); true } catch (e) { false }
if (ok) { clear attempt_at/count } else { attempt_count++; attempt_at = now }
Progression: 30 s → 1 min → 2 min → 4 min → 8 min → 16 min → 30 min (capped). With a permanent failure that is one request every 30 minutes instead of one a minute. A thirtyfold reduction.
The same applies to Vault.retryPendingBundles() — the same pattern.
snap_url is the external address of a camera, NVR or HTTP source. The client goes to that URL directly through Coil or ExoPlayer; our server merely passes the encrypted data_cipher containing the URL inside the bundle. No frames pass through us.
If an owner sets snap_int=1 for 10 guests, their NVR sees 10 requests a second. That is load on their NVR, not on our server.
Optionally: floor the interface slider at three seconds. That is a question of interface, not infrastructure.
Already reined in by the 6.61 fixes:
With ten BLE objects a user sends ten WebSocket messages in a row, and the server runs ten case 'ble_token_renew' branches in a row. Trivial.
File: GuestConn.kt — 7+ places.
AppState.overridesVersion = MutableState<Int>, a Compose signal meaning "the data changed, re-read remember". It is used in ActionCard:
val ovrVersion = AppState.overridesVersion.value
val (overrideLabel, avatarPath) = remember(action.key, ovrVersion) {
Prefs.getOverride(action.key) // SharedPreferences IO
}
val snapCfg = remember(action.key, ovrVersion) {
if (action.source == ActionSource.HOST_OWN) Prefs.getSnapshot(action.numberId)
else Prefs.getIncomingSnapshot(...)
}
val tpl = remember(action.key, ovrVersion) { loadTpl() } // more IO
Every bump makes each card re-read the override, avatar, snapshot and template from SharedPreferences. A user with 20 cards means 80 SharedPreferences reads per bump.
| Line | Context | Needed? |
|---|---|---|
| L193 | after re-ingesting a bundle (no K_obj) | yes |
| L355 | after parseNumbers in numbers_update | redundant when numbers have not changed |
| L411 | after an avatar is loaded | yes |
| L558, L595 | after parseNumbers in the other handlers | redundant when the data is identical |
With a flapping WebSocket, every guest_ok → the handler re-parses numbers → bumps overridesVersion → 80 SharedPreferences reads × 20 cards × the guest_ok rate.
That is local CPU and I/O, not direct load on the server, but:
Before bumping, compare the new JSON list of numbers with the previous one, by hash or by comparing the list of GuestNumberInfo). Identical → do not bump.
private var lastNumbersHash: Int = 0
private fun maybeUpdateNumbers(arr: JSONArray?) {
val parsed = parseNumbers(arr)
val newHash = parsed.hashCode() // or a hash of the JSON serialisation
if (newHash == lastNumbersHash) return // nothing changed
lastNumbersHash = newHash
numbers = parsed
publish()
AppState.overridesVersion.value = AppState.overridesVersion.value + 1
}
numbers_update arrives regularly but rarely actually changes — an owner does not edit objects every minute. The optimisation removes about 90% of the redundant redraw waves.