Skip to content

feat(signup): bounded + abortable post-signup user fetch#613

Draft
bird-m wants to merge 1 commit into
feat/signup-server-driven-fieldsfrom
feat/signup-bounded-user-fetch
Draft

feat(signup): bounded + abortable post-signup user fetch#613
bird-m wants to merge 1 commit into
feat/signup-server-driven-fieldsfrom
feat/signup-bounded-user-fetch

Conversation

@bird-m
Copy link
Copy Markdown
Collaborator

@bird-m bird-m commented May 7, 2026

Summary

Plumb a timeout and abort signal into the post-signup user fetch loop so the TUI auth gate can't hang after tokens are issued, and so user-initiated cancels (Esc, /exit, screen unmount) propagate end-to-end through the retry loop instead of running silently in the background.

This branch was carved out of the larger server-driven signup ceremony work (#539) so the bounded-fetch infrastructure can be reviewed in isolation. The persistence-side abort guard from #539 already prevents data integrity issues; this branch is a UX/observability quality pass that closes the "background work continues after Esc" wart.

What changed

  • src/lib/api.tsfetchAmplitudeUser accepts an options: { timeoutMs?, signal? } parameter and threads both into axios. Backwards-compatible (defaults to current behavior).
  • src/utils/signup-or-auth.ts
    • New UserFetchTimeoutError class for distinguishing user-fetch timeouts from generic errors.
    • New abortError(), isTimeoutOrAbort() helpers.
    • New abortableDelay(delayMs, signal) — replaces bare setTimeout for the retry sleep loop, with proper clearTimeout + removeEventListener cleanup in finally.
    • New fetchAmplitudeUserBounded(idToken, zone, signal) — wraps fetchAmplitudeUser in Promise.race with both an explicit 5s timer and a signal-abort listener (defense-in-depth in case axios's internal handling drops one).
    • fetchUserWithProvisioningRetry now accepts a signal and bails immediately on timeout/abort instead of continuing through the retry loop.
  • src/utils/__tests__/signup-or-auth.test.ts — adds a "fetchAmplitudeUser hangs → falls back to pending sentinel" test that exercises the bounded-fetch path under fake timers.

Why this isn't on the parent PR

#539 is already a sizable surgery on the signup ceremony (server-driven multi-step field collection, ceremony state reset across four entry points, etc.). The bounded-fetch work introduces ~140 lines of new abstraction (UserFetchTimeoutError, four helpers, retry-loop refactor, public-API change to fetchAmplitudeUser) that's worth its own focused review. Splitting it out keeps #539's diff scoped to "what changes about the user-facing ceremony" and keeps this PR scoped to "how do we cancel work cleanly when the user backs out."

Status

🚧 Draft, parked for now.

Base is set to feat/signup-server-driven-fields so the diff renders cleanly against the parent. Once #539 merges, this branch needs:

  1. git rebase main (base will switch from feat/signup-server-driven-fields to main).
  2. The single WIP preservation commit reorganized into proper focused commits at that point.

The WIP commit currently includes the union of items 2–5 from the same review pass that produced this work — those landed on #539 in their own commits, so the rebase will resolve those hunks as no-ops. The remaining diff at rebase time will be exactly the bounded-fetch infrastructure.

Test plan

  • After rebase: focused vitest run on src/utils/__tests__/signup-or-auth.test.ts
  • Manual: SigningUpScreen Esc during the success-arm provisioning retry loop — confirm the retry exits promptly instead of running for ~3.5s in the background
  • Manual: simulate a hung fetchAmplitudeUser (e.g. point at unreachable Data API) — confirm the wrapper times out at 5s and falls back to the pending sentinel rather than stranding the auth gate

🤖 Generated with Claude Code

Captures the reviewer-staged work for split-out into a focused PR.
Five items at this preservation point:

1. Bounded + abortable user fetch: api.ts adds {timeoutMs, signal}
   options; signup-or-auth.ts adds UserFetchTimeoutError,
   abortError(), isTimeoutOrAbort(), abortableDelay(),
   fetchAmplitudeUserBounded(). Retry loop bails immediately on
   timeout/abort.
2. In-memory signupAuth tokens (default.ts): TUI auth task uses
   session.signupAuth instead of getStoredToken from disk.
3. Fail-closed on premature success (SigningUpScreen.tsx): refuses
   tokens if ToS not accepted or required fields not satisfied.
4. Abort classification (direct-signup.ts isCallerAbort + wrapper
   short-circuit): caller aborts surface as code:'aborted' and skip
   signup_error telemetry.
5. Tests: two abort tests in direct-signup.test.ts, hang + abort
   tests in signup-or-auth.test.ts, cli.test.ts in-memory tokens
   test, new SigningUpScreen.test.tsx component test.

Items 2-4 will move to feat/signup-server-driven-fields. Item 1
stays here for its own follow-up PR after that branch merges. This
WIP commit will be reorganized into proper commits at rebase time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant