mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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>