Skip to content

Hosted Relay sockets: one Durable Object per account - #869

Merged
nedtwigg merged 4 commits into
mainfrom
remote-network-hosted-do
Oct 1, 2026
Merged

nedtwigg merged 4 commits into
mainfrom
remote-network-hosted-do

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Oct 1, 2026

Copy link
Copy Markdown
Member

Third of the Hosted persistent stack, on #868. It adds the Hosted Relay's sockets: /ws/burrow and /ws/client on relay.dormouse.sh, routed through one RelayRoom Durable Object per account.

Why per account, not per Burrow (decided 2026-09-30): Pocket keeps one relay socket for every Burrow it talks to and binds with init. An account-keyed object keeps that wire and Pocket unchanged. It also means an object only ever holds one account's sockets, so a cross-tenant binding can't happen.

The object:

  • Routing: reproduces the self-host RelayHub exactly: bind on init, client-gone, burrow-gone, and close codes 4000, 4001, 1008, 1009, 1013. The frame layer (guards, error strings, field-by-field envelopes) is now one shared module, remote-lib-common/src/remote/relay-routing.ts. One parity suite runs against both Relays.
  • Hibernation: WebSocket hibernation, with routing rebuilt from each socket's attachment and tags. Durable storage holds only the account id, which the object checks on every socket it accepts.
  • Frames: bounded in UTF-8 bytes before parsing. The object never reads ct; scripts/e2e-lint.mjs holds that textually, with self-test mutations.
  • Upgrades:
    • The Worker authenticates each upgrade and hands the object a fresh request carrying only the verified fields.
    • A Client upgrade requires the exact Origin; a Burrow upgrade refuses any Origin.
    • The object re-reads the Burrow's row before accepting, which closes the race with Remove.
  • Revocation: Remove at hosted.dormouse.sh closes the live socket through an RPC with no public route. A backstop alarm rechecks every held Burrow's row and its owner's entitlement hourly.
  • Liveness without waking the object: both ends send RELAY_PING every 30 s to the object's auto-response. Each end enforces its pong deadline only once a first pong has arrived, so an older self-host Relay costs nothing. The self-host Relay now answers the ping too.

Not verified against Cloudflare: the object reads Postgres through a RelayRows entrypoint via ctx.exports. In Miniflare, an object that opens a Hyperdrive connection itself can never hibernate again. The ctx.exports path works in Miniflare and wrangler deploy --dry-run, but hasn't been tried in production.

Test plan

  • Root pnpm test green. The Hosted Docker suite passed 128/129 just before a one-line rebase fix in a Docker-free test, and its runs before the rebase were green. It covers parity cases, hibernation eviction mid-routing, the alarm and sweep, tenant isolation, revocation, and a never-waking ping.
  • Manual (with the client PR): pair a phone against a preview and connect; hide the phone for a minute and return; remove the computer at the account page and see the phone lose it.

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: f07045c
Status: ✅  Deploy successful!
Preview URL: https://462e1895.mouseterm.pages.dev
Branch Preview URL: https://remote-network-hosted-do.mouseterm.pages.dev

View logs

@nedtwigg
nedtwigg force-pushed the remote-network-hosted-do branch from 6286a90 to 465ca9e Compare October 1, 2026 09:12

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is early feedback on a draft, not a merge verdict. Mark the PR ready when you want the full review.

There's one inline point: the object holds blockConcurrencyWhile while it waits on the row read through RelayRows, so a stalled database read could reset the whole account's object rather than fail only the one upgrade.

Comment thread hosted/server/relay-room.ts
@nedtwigg nedtwigg mentioned this pull request Oct 1, 2026
1 of 2 tasks
@nedtwigg
nedtwigg force-pushed the remote-network-hosted-do branch from 465ca9e to 2aeaad6 Compare October 1, 2026 09:32
@nedtwigg
nedtwigg added this pull request to stack #879 October 1, 2026 14:23
@nedtwigg
nedtwigg marked this pull request as ready for review October 1, 2026 18:35

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found nothing to change in the diff. The row read the draft review raised now gives up at RELAY_ROW_READ_TIMEOUT_MS and answers 503, and the stalled-read test covers that.

I'm leaving this as a comment rather than an approval because the Argos checks are red (argos/storybook-chromium, argos/storybook-webkit: 33 changed, waiting for a decision). The same 33-screenshot diff is on #867 and #868, and this PR touches no UI, so the diff most likely comes from the base of the stack. Accepting or rejecting it in Argos is up to a maintainer.

nedtwigg and others added 4 commits October 1, 2026 12:45
RelayRoom reproduces the self-host Relay's routing — bind on init,
client-gone, burrow-gone, 4000/4001/1008/1009/1013 — over WebSocket
hibernation, rebuilding from attachments and tags and storing only the
account id it checks every socket against. The relay Worker authenticates
each upgrade and hands the object only verified fields; removing a computer
closes its socket through an RPC with no public route. Both ends ping the
relay with an auto-response that never wakes the object, enforcing a deadline
only once a pong has arrived, and the self-host Relay answers it too. The two
Relays share one parity suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hourly

Review fixes: the object reads the Burrow row before accepting its socket and
every hour while it holds Burrows (through a RelayRows entrypoint, since a
Hyperdrive connection pins the object out of hibernation), so a removal race or
a lost entitlement closes the socket within the hour. Frames are bounded in
UTF-8 bytes, the frame layer is one shared module the e2e lint holds
textually, and one visibility handler drives both Pocket timers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nedtwigg
nedtwigg force-pushed the remote-network-hosted-do branch from 2aeaad6 to f07045c Compare October 1, 2026 20:17
@nedtwigg

nedtwigg commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

@dormouse-bot The maintainer has explicitly instructed us to IGNORE ARGOS for this stack and will handle the visual decisions separately. All other CI must pass, and we need your approval before merging. Please review current head f07045c and approve if no findings remain; please do not merge, since I am handling the stack bottom to top.

The account object's row read remains bounded by RELAY_ROW_READ_TIMEOUT_MS, with the stalled-read regression test. Your previous full review found no remaining code issue and withheld approval only for Argos; that gate is explicitly waived by the maintainer.

Base automatically changed from remote-network-hosted-relay to main October 1, 2026 20:30
@nedtwigg
nedtwigg merged commit bd761c1 into main Oct 1, 2026
12 checks passed
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.

2 participants