rastrillo / native Public

K2 follow-up: Retry-After survives wakes; stable streams timed from open open

On branch fix/k2-retry-deadline

Follow-up to #7, cut from main eb1689a. One commit, 67cd5ef. It fixes the two P2s Astra found in both apps' K2 branches (docs #67, sheets #38) once, here, as the coordinator asked.

The bugs

  1. A wake bypassed Retry-After. The apps passed Retry-After to Backoff.next as a floor and then waited with connectivity.wait, which any wake ends. So foregrounding or a path change cut a server's 30 s short.
  2. Unstable connections reset the backoff. The "stable stream" clock started with the attempt, so a slow refusal, a connect timeout or a 5 s access check counted as stable.

The fix

  • NativeReconnect (RastrilloApple, @MainActor) has three calls: opened() from the stream's onOpen, failed(error), and await pause(). - A .rateLimited(retryAfter:) becomes a not-before deadline, capped at 5 minutes. Only task cancellation ends it. - After the deadline, the pause waits out whatever remains of the backoff with connectivity.wait. A wake cuts that short and resets the backoff. If the path is down, it waits for the path. - now is injectable for tests.
  • Backoff.opened(at:) / ended(at:) replace connectionEnded(after:). The backoff resets only if the attempt opened and then stayed open longer than 5 s, and each open counts for one attempt only. connectionEnded was used only by the unmerged Docs and Sheets K2 branches, so removing it breaks no caller on any main.
  • RastrilloApple now depends on RastrilloNative, for Backoff. Apps already use both.
  • #7 nit. The README now says the kit string catalog is deferred to K3 and required there (English review, then the package's 12 languages, together with SyncStatus).

Tests, shown failing first

New tests:

  • ReconnectTests: - testRetryAfterSurvivesConnectivityWakes: a 1 s Retry-After, with a foreground and two path changes during it, still waits at least 0.95 s. - testRetryAfterThenWaitsForThePath - testStableLifetimeIsTimedFromOpen: with an injected clock, a 10 s attempt that never opened does not reset; open plus 6 s does. - testWakeCutsAnOrdinaryBackoffShort
  • BackoffTests.testAnAttemptThatNeverOpenedIsNeverStable. testResetsOnSuccessAndAfterAStableConnectionOnly moved to the new API.

Red.

  • A stub that reproduces both reported bugs (clock from the attempt's start; the deadline as an interruptible wait) failed the wake test ("A wake crossed the server's Retry-After", 0.16 s), the stable-lifetime test, and the never-opened Backoff test.
  • The two tests that guard existing behaviour failed against a second stub, a plain timer that ignores connectivity: "It retried with the path down", and 3.2 s where under 2 s was expected.

Green. make ci passes: swift test ran 62 tests with 0 failures, both companion builds succeeded, and the server and bundle tests passed. Hub CI will be red for the known infrastructure reason (no macOS runner, no unzip for Sparkle).

Docs #67 and Sheets #38 now use NativeReconnect and pin this branch until it merges. Stopping before merge.

LLM

Fixes the two shared P2s from Astra's review of docs #67 and sheets #38 once, in the kit: NativeReconnect keeps a Retry-After deadline that connectivity wakes cannot cross, and Backoff times stable streams from open. Also the #7 nit. make ci green locally.

Merging on Paul's instruction ("roll in any fixes, then proceed with deployment"). This branch fixes the two shared P2s from Astra's K2 reviews (docs #67, sheets #38) once, in the kit: Retry-After becomes a deadline that connectivity wakes can't cross, and the stable-lifetime timer starts at open. The coordinating session read the change. Local make ci passes (62 tests); hub CI is red for infrastructure reasons only. Merged as a merge commit, per AGENTS.md.