Public: anyone can read this; the crew can change it.

The device-code connect page polls on a timer: move it to a stream or a live-region swap #11024

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The device-code connect page no longer polls from the browser: the forge now asks the provider on the provider's own interval and pushes one event to the page. The allowlist entry for `pollJs` in `test/no-polling.test.ts` is gone. I ran `tsc`, `scripts/gate.ts`, `scripts/design-lint.ts` and `test/no-polling.test.ts`, `test/bring-oauth-return.test.ts`, `test/forge-origin-oauth.test.ts`, `test/opena…

shipped production is serving it deploy 95ed903

opened this

The device-code connect page no longer polls from the browser: the forge now asks the provider on the provider's own interval and pushes one event to the page. The allowlist entry for pollJs in test/no-polling.test.ts is gone. I ran tsc, scripts/gate.ts, scripts/design-lint.ts and test/no-polling.test.ts, test/bring-oauth-return.test.ts, test/forge-origin-oauth.test.ts, test/openapi-routes.test.ts, test/web-code.test.ts and test/keys-origins.test.ts; all pass. scripts/fast.ts named 1,584 test files, too many to run here, so the forge's check on the push is the full run.

What changed

  • New src/http/flow_stream.ts adds GET /settings/origins/flows/:id/stream, registered in src/http/settings_routes.ts. It is an event stream held open for the signed-in owner of the flow. Server-side, it calls the existing pollOAuthConnect on the flow's interval (including the slow_down backoff). When the flow is no longer pending it sends one flow.settled event with the status and closes. A missing or foreign flow answers 404, and a request with no signed-in user answers 401.
  • pollJs in src/web/client.ts is now an EventSource on the form's data-stream attribute that reloads the page on flow.settled. It has no timer, no fetch and no retry counter.
  • src/web/pages/origin-flow.ts emits data-stream instead of data-poll, for device flows only. The "Check again" form, the POST poll route and the no-JavaScript behaviour are unchanged.
  • In test/no-polling.test.ts I removed POLL_ALLOWED and pollReason, so any polling script is now an offender. The old test is replaced by one asserting pollJs is not flagged and uses EventSource. The new stream route is listed in NOT_WALKED because it needs a flow.
  • A new test in test/bring-oauth-return.test.ts checks that the page carries data-stream, that a signed-out request is refused, and that the stream ends itself with flow.settled. In that test the account is already connected from the earlier test, so the flow settles as failed rather than done. That still exercises the stream, and a done outcome is not asserted there.

What I left alone

  • The "Check again" button and its POST route, since they are the no-JavaScript path.
  • Several tabs open on one flow each hold their own server-side poll. That is harmless but not coalesced.

To try it Start a device-code connect at /settings/origins, approve it on the provider, and the page moves on without any repeating requests from the browser.

The first push was refused by the pre-push hook, which wants a Try: line in a commit message, so there is an empty Try: commit on the branch.

Closes #10964

commented

The device-code connect page no longer polls from the browser: the forge now asks the provider on the provider's own interval and pushes one event to the page. The allowlist entry for pollJs in test/no-polling.test.ts is gone. I ran tsc, scripts/gate.ts, scripts/design-lint.ts and test/no-polling.test.ts, test/bring-oauth-return.test.ts, test/forge-origin-oauth.test.ts, test/openapi-routes.test.ts, test/web-code.test.ts and test/keys-origins.test.ts; all pass. scripts/fast.ts named 1,584 test files, too many to run here, so the forge's check on the push is the full run.

What changed

  • New src/http/flow_stream.ts adds GET /settings/origins/flows/:id/stream, registered in src/http/settings_routes.ts. It is an event stream held open for the signed-in owner of the flow. Server-side, it calls the existing pollOAuthConnect on the flow's interval (including the slow_down backoff). When the flow is no longer pending it sends one flow.settled event with the status and closes. A missing or foreign flow answers 404, and a request with no signed-in user answers 401.
  • pollJs in src/web/client.ts is now an EventSource on the form's data-stream attribute that reloads the page on flow.settled. It has no timer, no fetch and no retry counter.
  • src/web/pages/origin-flow.ts emits data-stream instead of data-poll, for device flows only. The "Check again" form, the POST poll route and the no-JavaScript behaviour are unchanged.
  • In test/no-polling.test.ts I removed POLL_ALLOWED and pollReason, so any polling script is now an offender. The old test is replaced by one asserting pollJs is not flagged and uses EventSource. The new stream route is listed in NOT_WALKED because it needs a flow.
  • A new test in test/bring-oauth-return.test.ts checks that the page carries data-stream, that a signed-out request is refused, and that the stream ends itself with flow.settled. In that test the account is already connected from the earlier test, so the flow settles as failed rather than done. That still exercises the stream, and a done outcome is not asserted there.

What I left alone

  • The "Check again" button and its POST route, since they are the no-JavaScript path.
  • Several tabs open on one flow each hold their own server-side poll. That is harmless but not coalesced.

To try it Start a device-code connect at /settings/origins, approve it on the provider, and the page moves on without any repeating requests from the browser.

The first push was refused by the pre-push hook, which wants a Try: line in a commit message, so there is an empty Try: commit on the branch.

commented

The device-code connect page now waits on a stream the forge holds instead of a browser timer, so /settings/origins no longer needs a polling allowlist entry.

Changes

  • Stream route: GET /settings/origins/flows/:id/stream (src/http/flow_stream.ts) is the stream the connect page listens on. The client code in src/web/client.ts and src/web/pages/origin-flow.ts now uses it instead of polling. test/no-polling.test.ts drops its allowlist entry, and test/bring-oauth-return.test.ts gains a test for the stream.
  • Check fix: the last check failed because the new route was unknown to two route-sweep tests. I made two small test edits:

- test/security-floor-routes.test.ts: the route is now listed under UNSEEDED, because it needs a flow the seed doesn't create. It is still requested with a placeholder, so the security headers are checked on it. - test/pages-budget.test.ts: the route is now in NOT_PAGES, since it is a stream and not a page.

Those two tests and bun x tsc --noEmit pass here. I did not re-run the full suite; the forge's check does that on this push.

Left alone: the POST poll route /settings/origins/flows/:id/poll is still registered, which keeps this change to the stream swap.

To try it: open a device-code connect at /settings/origins and approve it on the provider. The page should move on by itself, with no reload loop.

🤖 Generated with Claude Code

Changes requested — before the latest push

The direction is right (no browser timer, allowlist gone), but three things must change before it lands. 1) Fix first: the stream's poll loop has no error handling and every open tab runs its own provider poll (see the comment on flow_stream.ts). 2) The success path is untested: the new test only proves 'failed'. 3) The data-stream attribute is built with raw() instead of being escaped. Also: the comment in the test claims 'someone else's flow: no stream' but only the signed-out case is tested; add the foreign-account case (404). I did not review a TUI door: if the terminal offers the connect flow, it needs the same behaviour, and a docs/features/ how-to line if the page's behaviour is documented.

commented

The device-code connect page now waits on a stream the forge holds, so the browser runs no timer. I've addressed all three of sentinel's points and pushed.

What changed since the first push

  • Poll loop (src/http/flow_stream.ts):

- One poller now runs per flow and every open tab shares it, so the provider is asked once per interval however many tabs wait. - A throw from pollOAuthConnect is caught and settles the flow as failed, so the page no longer waits forever with no signal. - A flow takes at most 8 tabs; a ninth gets 429. - A tab's stream ends after 15 minutes. It then sends flow.settled with status pending, so the page reloads and reopens a stream.

  • Escaping (src/web/pages/origin-flow.ts): the data-stream attribute is built with html, which escapes it, instead of raw().
  • Tests (test/bring-oauth-return.test.ts):

- The stream test now uses a fresh account (bob), since alice was already connected by the earlier test. - It asserts that nothing is emitted while the provider is pending, then that approval gives "status":"done". - It covers signed-out (401 or higher) and another account's flow (404, not an event stream).

  • Docs: I added a line to docs/ORIGINS.md, in the device-code step of the GitHub section, saying the forge polls once per flow and the page reloads once.

TUI: it never calls the connect-flow actions (pollOAuthConnect and startOAuthConnect appear only in actions, web pages and API), so no terminal change was needed.

Checks: bun scripts/gate.ts and bun x tsc --noEmit are clean. The bring-oauth, no-polling, pages-budget and security-floor tests pass. bun scripts/design-lint.ts printed only "design: no colour, size or component added outside the system", with no violations. I did not run the full suite; the forge's check does that.

Not done: there is no test for the poller-throws path or for two tabs sharing one poller. The fake provider can't be made to throw, and counting polls would need new seams, so both are covered only by review.

Try it: open a device-code connect at /settings/origins, approve it on the provider, and the page moves on once without a reload loop.

🤖 Generated with Claude Code

commented

The device-code connect page now waits on a stream the forge holds, so the browser runs no timer and its polling allowlist entry is gone.

What changed

  • Stream: GET /settings/origins/flows/:id/stream asks the provider once per interval and sends flow.settled with the status once. The page then reloads itself.
  • Shared poller: one poller runs per flow, shared by every tab waiting on it, so the provider is asked once per interval however many tabs are open. A flow takes at most 8 tabs. A tab's stream ends after 15 minutes with a pending status, which reloads the page and reopens the stream.
  • Provider errors: if the provider poll throws, the flow settles as failed and the stream closes, instead of leaving the page waiting with no signal.
  • Escaping: the data-stream attribute is built with html, which escapes it, instead of raw().
  • Failed check fix: the ninth tab's refusal now goes through tryAgain (429 with a retry-after of 30 seconds). It had been a bare 429, which is what failed test/retry-after.test.ts.
  • Tests: a fresh account (bob) exercises the real path: nothing is emitted while the provider is pending, then approval gives "status":"done". Signed-out and another account's flow (404) are also covered.
  • Docs: docs/ORIGINS.md says the forge polls once per flow and the page reloads once.

What I chose not to do

  • No test for the poller throwing or for two tabs sharing one poller, because the fake provider can't be made to throw and counting polls would need new seams.
  • No TUI change: the terminal never calls the connect-flow actions.

How to try it: open a device-code connect at /settings/origins, approve it on the provider, and the page moves on without a reload loop.

Checks: after this fix I re-ran only test/retry-after.test.ts, test/bring-oauth-return.test.ts and bun x tsc --noEmit, all green. bun scripts/gate.ts and bun scripts/design-lint.ts were last run before the fix; the forge's check runs everything.

🤖 Generated with Claude Code

Approved

Earlier asks are met. One poller now runs per flow, shared by all tabs. A throw from the provider poll settles the flow as failed. The data-stream attribute is built with html, so it is escaped. The stream test covers the pending, done, signed-out and foreign-flow cases. The ninth tab is refused through tryAgain, and docs/ORIGINS.md is updated (not README). The stream route checks the owner and answers 404 for another account's flow, and the browser runs no timer. The no-JS path is unchanged and there is no visible design change. Non-blocking: let lifetime in flow_stream.ts can be const. Non-blocking: add a test that two tabs share one poller. I could not diff against 4cf4bfc348f4 because that commit is not in this clone, so I judged the change as a whole.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs