Skip to content

feat(service,host): poll where Neon Realtime is not configured - #3943

Closed
charleslpan wants to merge 3 commits into
luke/neon-realtime-service-wakefrom
luke/neon-realtime-poll-fallback
Closed

charleslpan wants to merge 3 commits into
luke/neon-realtime-service-wakefrom
luke/neon-realtime-poll-fallback

Conversation

@charleslpan

@charleslpan charleslpan commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Stacks on #3266. Production has NEON_REALTIME_URL and NEON_REALTIME_SECRET and keeps running on Realtime. Previews and local dev have neither, and until now they heard nothing. This PR adds a polling fallback for them, chosen in one place per side, so no live area changes.

The fallback

  • Service wake (live/live-wake.ts): the wake that died when Realtime wasn't configured is now pollingWake. Its watch fires every LIVE_WAKE_POLL (1 s), so every held read checks again about once a second, which covers board look's 8 s deadline. A Realtime that refuses its configuration also falls back to polling. LIVE_SAFETY_POLL is unchanged, and its doc comment now says it is the only recovery from a refused or dropped Realtime watch.
  • Service sealing (live/realtime.ts, runtime.ts): without the variables, liveSealingFromEnvironment returns pollSealer. Its seal runs the statement through SqlClient and answers the usual shape at the sentinel LIVE_POLL_URL, with the rows inline. The layer now needs SqlClient. runtime.ts provides it with the same webSqlClient, and layer memoization keeps it to one pool. A secret that Realtime refuses still answers unavailable.
  • Wire (live-wire.ts): sealedLiveQuerySchema gains an optional rows. The wire's JSON Schema has no form for Unknown, so the rows travel as JSON text (fromJsonString). The live endpoints now encode their answers through the answer schemas, and the host already decodes with them. The JSON Schema goldens are regenerated with LUKE_UPDATE_FIXTURES=1, and the only change in them is rows: {type: string}. No new route or token was added: the rows only travel through /api/live/*, behind the bearer.
  • Host (live-follow.ts): when the address is LIVE_POLL_URL, openRealtimeClient returns a polling LiveClient. It hands on each seal's rows and skips a renewal whose rows haven't changed. liveRows re-seals every LIVE_POLL_EVERY (3 s) at that address. I put the cadence there rather than in a made-up expiresAt because an expiry close to now would make the renewal fire immediately, over and over.

Tests

  • live-app.test.ts: with the poll sealer, a plan inbox's rows come back at the poll address. Decoded through the wire schemas, they equal what taskInboxItems (the held read) returns.
  • realtime.test.ts: without the variables the sealer polls and returns rows. A refused secret still leaves it off.
  • live-wake.test.ts: with no wake configured, a held read checks again after 1 s. This replaces the "dies when not configured" test.
  • live-follow.test.ts (new): a polled read feeds liveRows and skips unchanged rows.

Checks run locally

Typecheck for features, service, host and web. Biome, oxlint, repository-checks.sh and knip. Vitest for the host, features and service projects, plus plan-comments.

Not confirmed

  • Rows match the held read on PGlite. I haven't checked them against Neon Realtime's own row encoding. Realtime keeps no order, and the readers sort, so order doesn't matter.
  • The account inbox polls 7 statements every 3 s per open desktop. That's fine for Previews and dev, but it isn't meant for production.

Made with Luke

View guided diff Turn on auto-fix

@vercel

vercel Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
luke-web Ready Ready Preview Oct 20, 2026 2:50am UTC

Request Review

charleslpan and others added 2 commits October 20, 2026 02:32
A deployment without NEON_REALTIME_URL and NEON_REALTIME_SECRET (a
Preview, local dev) now polls behind the same interfaces rather than
hearing nothing: the service's LiveWake wakes every held read about once
a second, its sealer reads each live statement on the database client
and answers the rows inline at LIVE_POLL_URL, and the host's client at
that address hands those rows on, sealing again every three seconds and
skipping a poll whose rows did not change.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…og as an error

Previews and local dev poll quietly; production should never poll, so an
instance there that finds the Realtime variables missing logs an error
at start instead of polling silently.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
@luke-agent

luke-agent Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Closing as superseded by #4172 (with #4167), now on main. The wire's LIVE_POLL_URL and inline rows, the service's pollSealer, and sealedParts polling when Realtime is unconfigured, plus the host's liveRows polling fallback, all cover what this added. Two pieces didn't carry over: pollingWake for live-wake.ts, which lives with #3266 and goes there if that's still wanted, and a one-line production log when Realtime is unconfigured. The branch is kept.

@luke-agent luke-agent Bot closed this Oct 20, 2026

This branch was successfully deployed

1 active deployment
Preview — 8acbd211 Deployed Oct 20, 2026 by vercel[bot]
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