Files
64144d2f4f perf(desktop): move the local-archive subscription into Rust
The renderer owned the whole archive path: it listed saved subscriptions,
opened a live REQ per scope, buffered inbound frames, batched them, and
handed each batch back over IPC to be written to SQLite. Every archived
frame therefore made a round trip into JS for no reason other than
history — nothing in that path is a rendering concern.

`archive::sync` now runs the whole pipeline natively. It subscribes from
the saved subscriptions, buffers, flushes on the same thresholds the JS
manager used (FLUSH_BATCH_SIZE 25 / 2000ms deadline — parity, not a
retune), archives via SQLite directly, and emits
`archive-agent-metrics-changed` when a batch actually persisted metric
rows. `archiveSyncManager.ts` is deleted.

`native_relay_client` is the shared piece underneath: one authenticated
socket per (relay, pubkey), multiplexed subscriptions, declarative
`set_subscriptions` reconciliation, and exponential reconnect backoff.
A 30s read timeout is idle, not failure, and is discriminated by error
variant rather than message text so a reworded error cannot turn every
quiet period into a reconnect storm.

A relay CLOSED drives its own recovery: per-id retry state lives next to
`open` in the connection (not in `desired`, which is reloaded from
SQLite and would resurrect a deletion), a dedicated select! deadline arm
fires the reopen and is disabled when nothing is scheduled, and the
CLOSED message is classified terminal / rate-limited / retryable with
the same prefixes the renderer used, `auth-required:` deliberately
retryable. Rate-limited arms the shared relay_admission gate and waits
max(backoff, hint); backoff is 1s→30s saturating; attempts reset on
EVENT or EOSE. Terminal suppression is per-socket by design: a
reconnect retries a terminal id once through the normal path, because
relay policy can change and one REQ per reconnect is bounded.

The sync lifecycle is owned, not raced. The renderer allocates a
monotonic lease synchronously in effect order — intent order, which IPC
completion order is not — and Rust ignores any start/stop older than
the highest (epoch, lease) mark it has seen; a stop advances the mark,
so a delayed start cannot resurrect a stopped task. Above the lease,
Rust mints a realm epoch, published atomically with minting under the
same lock that orders lifecycle calls: announcing IS what supersedes
the previous realm, so a separately-held counter would leave a window
in which a dead realm's delayed calls still win. The renderer awaits
the epoch before its first lifecycle command. Ownership is
main-window-only via the established huddleWindowChannelId() exclusion:
a huddle companion mounts the same tree in a concurrent realm, and
concurrent owners cannot be ordered by any newest-wins clock.

What stays in JS is the start gate, deliberately. Kind 24200 is
relay-ephemeral, so frames emitted before the listener opens are lost
permanently, and only the renderer knows when observer reconciliation
finished seeding 24200 into the saved subscription. The backend task is
therefore not self-starting: `useArchiveSync` starts it once
reconciliation resolves and stops it on unmount.

`RelaySession.revision` was documented as rejecting stale in-flight
reconciliation but never did — removed rather than repaired:
declarative reconciliation re-reads the desired set every pass, so
there is no generation to guard.

Tests: archive/sync_tests.rs drives the real run_sync body through a
fake IO seam (filter parity, flush thresholds, failure isolation) plus
the ownership contract (out-of-order start/stop both directions,
announcement supersedes a dead realm's delayed calls before any new
lifecycle call, publish-(epoch,0) does not lock out the announcing
realm). native_relay_client's stub-relay test completes the NIP-42
handshake over a real TCP socket, injects CLOSED with the desired set
unchanged, and proves the REQ is retried by the deadline arm — the
relay-backed #[ignore] test additionally proves the REQ shape against a
real relay. useArchiveSync.test.mjs owns the start gate, realm
ownership, and post-reload realm supersession via fresh module
instances. observer-archive-policy.spec.ts owns wiring, with payload
receipts that announce precedes start and start carries a numeric
epoch. Every claim was mutation-checked; the vacuous first drafts
(wake-masked reopen, policy re-implementation, precondition-rebuilding
supersession, same-scope no-op escape) were each caught by their
mutants and rewritten.

Includes one move-only hunk that is not archive work: the push-to-talk
global-shortcut handler moves from lib.rs into `ptt_shortcut::install`,
mirroring the existing `app_menu::install` seam, paying the 1000-line
ratchet budget in the module that owns the registration lifecycle. The
handler body is proven token-identical with a mutated-body negative
control. lib.rs is 917 lines.

Co-authored-by: Tyler Longwell <tlongwell@squareup.com>
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
2026-08-15 21:38:48 -04:00
..