Repository navigation
Ssr plugin wip #6800
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
benedikt-bartscher
wants to merge
27
commits into
reflex-dev:main
Choose a base branch
from
benedikt-bartscher:ssr-plugin
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Ssr plugin wip #6800
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
2272cc0
wip
benedikt-bartscher f120b60
Merge remote-tracking branch 'upstream/main' into ssr
benedikt-bartscher 0606e2b
fix: add fixture to clean state subclasses after tests
benedikt-bartscher 32bd4a7
make ssr deps optional
benedikt-bartscher 1158c3c
add integration test
benedikt-bartscher 0287832
fix typing
benedikt-bartscher 263d3eb
codespell
benedikt-bartscher fe877a6
fix
benedikt-bartscher 9369532
fix hashes
benedikt-bartscher 2feca15
fix js formatting
benedikt-bartscher 0274b8e
cache this
benedikt-bartscher d3d9c74
Merge remote-tracking branch 'upstream' into ssr
benedikt-bartscher 257ed79
wip
benedikt-bartscher 8411139
plugin based ssr
benedikt-bartscher 8da2d30
Merge remote-tracking branch 'upstream/main' into ssr-plugin
benedikt-bartscher ec14b54
isr wip
benedikt-bartscher 1b735e5
test
benedikt-bartscher 0f7d74b
ruff
benedikt-bartscher 9096d34
Merge remote-tracking branch 'upstream/main' into ssr-plugin
benedikt-bartscher 9d62492
Merge remote-tracking branch 'upstream/main' into ssr-plugin
benedikt-bartscher 9c86da6
Fix SSR load inputs, catch-all params and ISR lock ownership
benedikt-bartscher 0893414
Hoist ISR and SSR imports, document handlers and add news fragments
benedikt-bartscher a51c82e
Merge remote-tracking branch 'upstream/main' into ssr-plugin
benedikt-bartscher c87e5ff
Merge remote-tracking branch 'upstream/main' into ssr-plugin
benedikt-bartscher e763577
Install express for the ISR render service
benedikt-bartscher 01f9053
Decode SSR route params like React Router
benedikt-bartscher 1c6edaa
Cache ISR pages by raw path and drop the unimplemented bot-only mode
benedikt-bartscher File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add experimental, default-off server-side rendering: setting the new `ssr_mode` config option (`rx.SsrMode`) adds an `/_ssr_data` endpoint that runs a page's `on_load` handlers, so server-rendered pages contain their data. The new `reflex.isr` page server (`granian --factory reflex.isr:create_isr_app`) caches those pages in Redis with stale-while-revalidate, and `reflex.isr.revalidate_path`, `reflex.isr.revalidate_tag` or `POST /_isr/revalidate` invalidate them on demand. Pages are cached per path without the query string; a page's `on_load` fills in query-dependent content after hydration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add the experimental, default-off `ssr_mode` config option with the frontend templates that render pages on the server, and the `isr_render_url`, `isr_revalidate` and `isr_build_id` options that configure incremental static regeneration, which renders and caches pages per path without the query string. |
109 changes: 109 additions & 0 deletions
109
packages/reflex-base/src/reflex_base/.templates/web/isr-render.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| /** | ||
| * ISR render tier for Reflex. | ||
| * | ||
| * A small HTTP service that renders the react-router **server bundle** to HTML | ||
| * for a given route, so the Python ISR page-server (reflex.isr.HttpRenderer) | ||
| * can cache and serve it. This is the one component that must run JavaScript; | ||
| * it is deployed as its own service (off the request hot path, scale-to-zero | ||
| * friendly) rather than in every frontend pod. | ||
| * | ||
| * Contract (matches reflex.isr.HttpRenderer): | ||
| * POST / { "path": "/blog/hello" } | ||
| * 200 -> { "html": "<!DOCTYPE html>…", "revalidate"?: number, "tags"?: string[] } | ||
| * non-200 -> ISR treats it as a render failure (serves stale/shell) | ||
| * | ||
| * Requirements: | ||
| * - An SSR build of the app (react-router build with `ssr: true`) producing | ||
| * `build/server/index.js`. The app root `loader()` fetches page state from | ||
| * the Python `/_ssr_data` endpoint, so this service needs network access to | ||
| * the backend (set SSR_DATA / the backend URL in the app env). | ||
| * - Node deps `react-router` and `express`, which the SSR build installs. | ||
| * | ||
| * Run: node isr-render.mjs (listens on $PORT, default 8600) | ||
| * | ||
| * Optional per-page cache hints: the app may set response headers | ||
| * `X-Reflex-Revalidate: <seconds>` and `X-Reflex-Tags: a,b,c` | ||
| * which are forwarded to the ISR cache; otherwise Python applies its defaults. | ||
| */ | ||
| import { createRequestHandler } from "react-router"; | ||
| import express from "express"; | ||
| import { dirname, join } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const __dirname = | ||
| import.meta.dirname ?? dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| const serverBuild = join(__dirname, "build", "server", "index.js"); | ||
| const build = await import(serverBuild); | ||
| const handler = createRequestHandler(build, "production"); | ||
|
|
||
| const app = express(); | ||
| app.use(express.json({ limit: "1mb" })); | ||
|
|
||
| // Health endpoint for k8s probes. | ||
| app.get("/_health", (_req, res) => res.status(200).send("OK")); | ||
|
|
||
| app.post("/", async (req, res) => { | ||
| const path = | ||
| typeof req.body?.path === "string" && req.body.path ? req.body.path : "/"; | ||
|
|
||
| try { | ||
| // Render the target route through react-router as a normal GET document | ||
| // request. The app's loader runs here and pulls state from /_ssr_data. | ||
| const url = new URL(path, "http://reflex-isr-render.local"); | ||
| const request = new Request(url, { | ||
| method: "GET", | ||
| headers: { | ||
| "User-Agent": "ReflexISRRenderer/1.0", | ||
| Accept: "text/html", | ||
| // Forward the caller's cookies so authenticated/personalized loaders | ||
| // resolve the same way they would for the original visitor, if the | ||
| // page-server chooses to pass them along. | ||
| ...(req.body?.cookie ? { Cookie: String(req.body.cookie) } : {}), | ||
| }, | ||
| }); | ||
|
|
||
| const response = await handler(request); | ||
| const html = await response.text(); | ||
|
|
||
| if (!response.ok || !html) { | ||
| return res | ||
| .status(502) | ||
| .json({ error: "render_failed", status: response.status }); | ||
| } | ||
|
|
||
| const revalidate = headerNumber(response, "x-reflex-revalidate"); | ||
| const tags = headerList(response, "x-reflex-tags"); | ||
|
|
||
| return res.json({ | ||
| html, | ||
| ...(revalidate != null ? { revalidate } : {}), | ||
| ...(tags.length ? { tags } : {}), | ||
| }); | ||
| } catch (err) { | ||
| console.error("[isr-render] render error for", path, err); | ||
| return res.status(500).json({ error: String(err) }); | ||
| } | ||
| }); | ||
|
|
||
| function headerNumber(response, name) { | ||
| const raw = response.headers.get(name); | ||
| if (raw == null) return null; | ||
| const n = Number(raw); | ||
| return Number.isFinite(n) ? n : null; | ||
| } | ||
|
|
||
| function headerList(response, name) { | ||
| const raw = response.headers.get(name); | ||
| return raw | ||
| ? raw | ||
| .split(",") | ||
| .map((s) => s.trim()) | ||
| .filter(Boolean) | ||
| : []; | ||
| } | ||
|
|
||
| const port = parseInt(process.env.PORT || "8600", 10); | ||
| app.listen(port, () => { | ||
| console.log(`[isr-render] listening on http://localhost:${port}`); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.