# Server Resolution Prefetch Design

Date: 2026-05-28

## Purpose

Reduce perceived server join latency by resolving the active TeamSpeak server address before the user taps Connect. Prefetch must be invisible, conservative, and safe: it may warm resolver state, but it must not change connection semantics or surface background errors to the user.

Recent Android testing showed resolver latency can dominate the first part of the connect flow. A prior fix bounded slow TS3 SRV discovery and removed Android's forced Cloudflare resolver. Prefetch builds on that by hiding remaining address-resolution work when the user has already entered or loaded a likely server address.

## Goals

- Prefetch only the active server address field.
- Keep the feature invisible to users.
- Reuse prefetched results only for exact normalized host matches.
- Keep prefetched results fresh for 2 minutes.
- Preserve today's Connect behavior when prefetch misses, fails, or is stale.
- Avoid prefetching all bookmarks.
- Avoid opening a TS3 session before the user taps Connect.

## Non-Goals

- No visible resolving, ready, or failed UI state.
- No bookmark fan-out prefetch.
- No persisted resolver cache across app launches.
- No password, channel, or permission validation during prefetch.
- No server reachability probe beyond address resolution.
- No connection warm-up or pre-authentication.

## Chosen Approach

Use a Rust-owned resolver prefetch cache with Flutter-owned scheduling.

Flutter knows when the active host field changes, so it schedules prefetch requests. Rust owns resolver correctness, normalization, cache validity, and connect-time reuse. This keeps Flutter from depending on resolver internals and ensures Connect can independently decide whether a prefetched result is safe to use.

Other approaches considered:

- Flutter-only prefetch: rejected because it pushes resolver state into Dart and creates a weaker boundary between UI and connection behavior.
- Resolver-internal repeated-call cache only: rejected because it does not hide first-click latency from the active host field.

## Behavior

Prefetch starts for the active host value in two cases:

- After `_loadUiSettings()` loads the last-used host into `_hostCtl`.
- After the user stops editing the host field for about 700 ms.

The feature is invisible:

- No SnackBars.
- No inline status text.
- No disabled Connect button.
- No user-facing error if prefetch fails.

Connect behavior:

- If the current normalized host exactly matches a fresh prefetched entry, Connect uses the cached resolved address.
- If the cache is missing, stale, failed, or for a different host, Connect resolves normally.
- Connect remains the only operation that opens a TS3 session.

## Architecture

### Flutter Scheduling

`_BetaHomeState` owns the host text field. It should add a listener to `_hostCtl` and manage a short debounce timer.

Responsibilities:

- Trim the host input before scheduling.
- Skip empty values.
- Reset the debounce timer on each edit.
- Call a bridge prefetch API after about 700 ms of idle typing.
- Schedule one prefetch after settings load if the loaded host is non-empty.
- Dispose the listener and timer with the widget state.

Flutter does not store resolved addresses and does not decide whether Connect can use a prefetched result.

### Bridge API

Add a fire-and-forget bridge function shaped like:

```text
prefetch_server_resolution(host: String) -> Result<(), BridgeError>
```

The bridge call should return after the prefetch task has been accepted by the Rust runtime. It must not wait for resolution to complete. Background completion or failure is reported only through diagnostics/logging.

### Rust Resolver Cache

Rust stores a small prefetch cache owned near the session/resolver boundary. A single latest-host entry is enough for v1, because the design only prefetches the active field.

Cache entry fields:

- Normalized input host.
- Resolved `host:port` address.
- Resolution method.
- Completion timestamp.
- Generation or request id.
- Optional sanitized failure metadata for diagnostics.

The cache TTL is 2 minutes.

### Connect Integration

Connect should ask Rust for a fresh exact-match prefetched result before running normal resolution.

Rules:

- Exact normalized host match is required.
- Entry age must be at most 2 minutes.
- Failed entries must not block normal connect resolution.
- Stale entries must be ignored.
- Missing cache must behave exactly like today.

## Data Flow

1. App starts.
2. `_loadUiSettings()` loads the last-used host into `_hostCtl`.
3. Flutter schedules invisible prefetch for that host.
4. User edits the host field.
5. Flutter cancels the pending debounce timer and starts a new one.
6. After 700 ms idle, Flutter calls Rust prefetch with the latest trimmed host.
7. Rust normalizes and resolves the host through the same resolver path used by Connect.
8. Rust stores the result if it still matches the latest generation for that normalized host.
9. User taps Connect.
10. Rust Connect checks the cache for a fresh exact-match result.
11. Cache hit: Connect uses the prefetched address.
12. Cache miss/stale/failure: Connect resolves normally.

## Cancellation And Staleness

Cancellation can be logical rather than hard task cancellation.

- Flutter prevents obsolete debounce timers from firing.
- Rust tags requests by normalized host and generation.
- Late completions for stale generations must not replace newer successful entries.
- Duplicate prefetches for the same normalized host may coalesce or refresh the same entry.

This avoids complexity while preventing old input values from poisoning the cache.

## Error Handling

Prefetch failures are diagnostic-only.

- Empty host: skip prefetch.
- Invalid host shape: skip or fail silently with debug diagnostics.
- Resolver failure: store optional sanitized failure metadata for diagnostics only.
- Connect after failure: normal connect path runs and surfaces errors as it does today.
- App resume and network changes: no special invalidation in v1; TTL handles staleness.
- Disconnect: cache may remain because it is independent of the TS3 session.

## Diagnostics

Add privacy-safe logs for:

- Prefetch started.
- Prefetch result.
- Prefetch failed.
- Connect using prefetched resolution.
- Connect prefetch miss or stale entry.

Do not log passwords, channel passwords, or nickname. Host and resolved address are acceptable because resolver/connect logging already includes them today.

## Testing

Rust tests:

- Fresh exact-match prefetched result is reusable.
- Stale prefetched result is ignored.
- Different normalized host is ignored.
- Failed prefetch does not block normal resolution.
- Late stale generation cannot overwrite a newer cache entry.

Flutter tests should cover the scheduling logic through a small testable helper if wiring directly through `_BetaHomeState` would be brittle:

- Host edits debounce prefetch scheduling.
- Empty host does not prefetch.
- Settings-loaded host schedules one prefetch.

Manual Android smoke test:

- Install debug APK.
- Launch app.
- Wait for last-used host prefetch or type host and wait past debounce.
- Tap Connect.
- Confirm UI reaches connected server view.
- Confirm logcat shows either a prefetch cache hit or safe fallback behavior.

## Acceptance Criteria

- Typing or loading a valid host can warm resolver state before Connect.
- Connect never fails because prefetch failed.
- Connect never uses a prefetched result for a different normalized host.
- Prefetched entries older than 2 minutes are ignored.
- No visible UI is added for prefetch state.
- Bookmarks are not prefetched in bulk.
- Android debug build and focused resolver/Flutter tests pass.

## Implementation Notes

- Prefer a single latest-host cache unless implementation reveals an existing cache abstraction that makes a tiny map simpler.
- Prefer minimal bridge API surface: one prefetch call and connect-time internal cache lookup.
- Keep the resolver cache near existing Rust session/connect code so future non-Flutter clients can benefit from the same behavior.
