Skip to content

Connect: parallel gate lookups, zombie-socket freshness, gate fonts, per-message compression (B29-B32) - #1909

Draft
SawyerHood wants to merge 4 commits into
mainfrom
bb/mobile-perf/connect-tunnel
Draft

Connect: parallel gate lookups, zombie-socket freshness, gate fonts, per-message compression (B29-B32)#1909
SawyerHood wants to merge 4 commits into
mainfrom
bb/mobile-perf/connect-tunnel

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

What was wrong

  • The connect gate awaited the label lookup and then the session lookup, two single-region D1 round trips in series on every uncached visitor request. Its per-isolate caches stored only settled values, so a page load's burst of asset requests all missed together and each ran its own query.
  • TunnelDO treated any tunnel socket with readyState === OPEN as live. A client whose network died without a close frame left a zombie that still accepted send(). Every visitor request was proxied into it and hung 30 s before a 504, for as long as the zombie lingered (up to ~80 s).
  • The 401/503 gate pages loaded a Google Fonts stylesheet, a render-blocking cross-origin fetch before first paint on a phone.
  • The tunnel client sent every relayed frame through permessage-deflate, including body chunks that the origin had already brotli/gzip encoded.

What changed

  • apps/connect/src/worker.ts: visitor cookie verification starts before await resolveLabel and overlaps it (skipped on tunnel dials). apps/connect/src/session.ts: label and session caches also share the in-flight promise; a rejected lookup is dropped so the next request retries.
  • apps/connect/src/tunnel-do.ts: liveness now requires max(acceptedAt, getWebSocketAutoResponseTimestamp) <= TUNNEL_STALE_MS (50 s). acceptedAt is stored in the socket attachment (survives hibernation). Stale sockets are closed; if no live socket remains, in-flight streams fail at once and the presence alarm stops. Sockets accepted before this change are stamped on first sight and get one grace window. packages/tunnel-client: heartbeat deadline 60 s -> 45 s (detects a dead link at the third 20 s tick, 60 s, instead of 80 s).
  • Gate pages use the system font stack; no third-party stylesheet.
  • TunnelSession.send takes a compress option; body chunks for responses with a non-identity Content-Encoding are sent with compress: false. Identity bodies and control frames keep deflate (deviation from "all body-chunk frames": small uncompressed responses still benefit). apps/host-daemon/src/ws.d.ts gains the send(data, options) overload.

How you verified

  • New tests (fail before, pass after): apps/connect/src/worker.test.ts (gate lookup overlap: session verified while label pending, no session check on tunnel dial, no unhandled rejection on 404; gate page has no font link; TunnelDO zombie sockets: 503 + close for stale, live with recent heartbeat, live just after accept, legacy grace window, in-flight streams fail immediately, route around zombie to fresh replacement without abandoning streams, alarm drops presence). apps/connect/src/session.test.ts (5 concurrent resolves -> 1 query; fresh not shared; rejected lookup retried; 5 concurrent session verifications -> 1 query; real in-memory SQLite). packages/tunnel-client/test/session-compress.test.ts (precompressed body chunks compress:false, identity compress:true, control frames compress; isPrecompressedResponse parsing).
  • pnpm exec turbo run typecheck --filter=@bb/connect --filter=@bb/tunnel-client --filter=bb-plugin-connect --filter=@bb/host-daemon: 4/4 successful.
  • pnpm exec turbo run test --filter=@bb/connect --filter=@bb/tunnel-client --filter=bb-plugin-connect: 112 / 15 / 78 passed. pnpm exec turbo run test --filter=@bb/host-daemon -- src/connect-tunnel: 8 passed.
  • pnpm exec eslint and prettier --check on changed files: clean.

Fixes: part of the mobile / iOS Safari performance program (verified sweep report in the bb thread; no single issue).

Stack context

Standalone PR (no overlap with the bb/mobile-perf/* stack); targets main.

  • Audit findings addressed: see title IDs. Review: approved-with-fixes; 1 review fix commit(s).
  • Deliberately not done here: B29 optional 'D1 read replication' - infra/wrangler configuration, not a code change; left as a follow-up.
  • Reviewer notes / follow-ups: B30 relies on the runtime preserving getWebSocketAutoResponseTimestamp across hibernation (documented Cloudflare behaviour, but unverifiable in unit tests). Failure mode is a visib | The relay window (TUNNEL_STALE_MS=50s in apps/connect) and client heartbeat interval/deadline (20s/45s in @bb/tunnel-client) are coupled only by comments; no cross-package test gua
  • Wire/contract: No server<->host-daemon wire change (HOST_DAEMON_PROTOCOL_VERSION untouched). Tunnel wire (relay<->connect client) frame format unchanged; only ws-level per-message compression flag and heartbeat timings changed. TunnelDO tunnel-socket attachment gains {acceptedAt} (previously none); sockets without it are handled (stamped on first sight). No CLI/config/plugin-API surface changes.

AGENT GENERATED: by Claude Code (claude-mangosteen-eap)

SawyerHood and others added 4 commits August 19, 2026 07:31
The connect gate resolved the label, then verified the visitor's session,
each a single-region D1 round trip awaited back to back on every uncached
request. The per-isolate caches also held only settled values, so a page
load's burst of asset requests all missed together and each issued its own
query before the first answer landed. The 401/503 gate pages additionally
blocked first paint on a cross-origin Google Fonts stylesheet.

Start visitor cookie verification before awaiting the label lookup so the
two overlap (tunnel dials skip it), share the in-flight promise per label
and per cookie so a burst costs one query each, and render the gate pages
with the system font stack.

Co-Authored-By: Claude <noreply@anthropic.com>
A tunnel client whose network died without a close frame (laptop lid,
cellular handoff, NAT timeout) leaves its socket in the DO with readyState
OPEN and a working send(). tunnelSocket() only checked readyState, so every
visitor request was proxied into the void and hung for the 30s response
head timeout before a 504, for as long as the TCP zombie lingered (~80s).

Liveness now also requires the later of the socket's accept time (stored in
its attachment so it survives hibernation) and the runtime's last heartbeat
auto-response timestamp to be within TUNNEL_STALE_MS (50s, two missed 20s
client pings plus slack). Stale sockets are closed so visitors get the
offline page immediately, their in-flight streams fail at once instead of
waiting out the timeout, and the presence alarm stops advertising them. A
socket accepted before this change (no timestamp of either kind) is stamped
on first sight and earns one grace window. The client heartbeat deadline
drops from 60s to 45s so a real drop is detected at the third tick (60s)
rather than the fourth (80s), one tick after the relay goes offline.

Co-Authored-By: Claude <noreply@anthropic.com>
The tunnel dial negotiates permessage-deflate and every relayed frame rode
it, including body chunks whose origin response was already brotli/gzip
encoded (static assets, compressed API JSON). Deflating those again cost
CPU per chunk on the host and slightly grew the frames.

TunnelSession.send now takes a compress option; executeHttp passes
compress: false for body chunks when the origin response carries a
non-identity Content-Encoding. Identity bodies and all control frames keep
the extension. This deviates from the "all body-chunk frames" suggestion so
uncompressed origin bodies (small responses below the server's compression
threshold, plain-text tool output) still benefit from deflate on the wire.
The host daemon's hand-written ws type shim gains the send options overload
so it typechecks the shared session.

Co-Authored-By: Claude <noreply@anthropic.com>
startVisitorAuth fires the session verification before the label lookup so
the two D1 round trips overlap. When the gate returns before awaiting it
(unknown label 404, machine info page served from the label cache), the
pending D1 read was left dangling with no waitUntil, so the request context
could close on it. session.ts now shares that pending lookup with every
later request carrying the same cookie, so a never-settling promise would
strand those requests. Register the promise with ctx.waitUntil, as the gate
already does for markMachineSeen, and cover the early-return path.

Co-Authored-By: Claude <noreply@anthropic.com>
@SawyerHood
SawyerHood marked this pull request as ready for review August 19, 2026 08:04
@bb-slop-cop

bb-slop-cop Bot commented Aug 19, 2026

Copy link
Copy Markdown

🚨 SLOP COP 🚨 · review

I am SlopCop. I am reviewing this PR now.

I will check security, code quality, performance, architecture, duplicate code, and the changed route.

cookieHeader,
runtime.desktopSessionCookieName,
);
const visitorAuth = isTunnelDial

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/reviewMedium: Bound the eager-auth cache.

This code starts session checks before the route needs them. Unknown labels and public routes still run HMAC work for each dotted cookie. verifySessionCookie caches each unique invalid value. Expired keys remain until the same key returns. Public requests can grow the isolate memory without a limit. Please skip auth for public routes and add a cache limit or cleanup.

// last heartbeat it answered (TUNNEL_STALE_MS in apps/connect), so the
// client redials within one tick of the relay showing its visitors the
// offline page instead of ~30s later.
const HEARTBEAT_DEADLINE_MS = 45_000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/reviewLow: Add a direct heartbeat timer test.

This deadline changes the disconnect time from 80 seconds to 60 seconds. Current tests do not check the new third-tick boundary. Please add a fake-timer test that confirms the socket closes at the third interval.

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 SLOP COP 🚨 · review

Plain-English summary

This PR makes the remote bb connection path faster and more reliable. It combines duplicate database reads and rejects old sockets sooner. It also removes external gate fonts and avoids repeat compression.

Findings

  • Medium: Visitor auth starts before the route needs it. Unknown labels and public routes still run HMAC work. Unique invalid cookies also fill an unbounded cache. Skip auth for public routes, and add a cache limit or cleanup.
  • Low: The new heartbeat deadline changes the disconnect time from 80 seconds to 60 seconds. Add a direct timer test for this boundary.

Other review results

  • I found no authentication bypass, trust-boundary error, or compression security issue.
  • The socket freshness and compression paths have no other performance issue.
  • shareInflight duplicates the server createAsyncDeduper pattern. This is a small refactor option, not a required change.
  • The client and relay keep related heartbeat times in separate packages. A shared contract could prevent future drift.
  • The host daemon protocol version does not need a change. This PR changes no server-daemon wire data.

Checks

  • Connect passed 113 tests.
  • Tunnel client passed 15 tests.
  • Connect, tunnel client, and host daemon passed Turbo type checks.
  • The local Wrangler gate returned the expected 401 sign-in page.
  • Browser QA confirmed zero external stylesheets and the system font stack.
  • git diff --check origin/main...HEAD passed.

I posted the two specific items as inline comments. I used a comment review, as required by this rule.

@SawyerHood
SawyerHood marked this pull request as draft August 19, 2026 15:09
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