SSR docs declare the embedded-V8 architecture; surface re-chartered per appeal
docs/SSR_ARCHITECTURE.md now specifies the decided shape: Django template backend rendering through the PyO3-bound SsrEngine (cores/mizan-rust-ssr, deno_core + deno_web), bundles built by mizan-generate, props crossing as parsed values, no external JS runtime serving requests, no_rsc guard. docs/AFI_ARCHITECTURE.md follows (mizan-rust-ssr in the cores layout, Bun worker delisted). OWED_SURFACE.md regenerated via appeal-surface: the Bun→PyO3 cutover, the PyO3 binding surface, and the mizan-generate SSR bundling step are now chartered owed mechanisms instead of an orphaned engine crate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,20 +28,21 @@ cores/ shared language-level primitives
|
||||
mizan-python/ @client decorator, registry, MWT, HMAC cache keys
|
||||
mizan-rust/ Rust core — IR build (build_ir()), registry
|
||||
mizan-rust-macros/ #[derive(Mizan)] / #[mizan::client] proc-macros
|
||||
mizan-rust-ssr/ embedded-V8 SSR engine (deno_core + deno_web); evals the
|
||||
build-time bundle, renders per request; no_rsc guard
|
||||
protocol/ protocol-level tooling
|
||||
mizan-codegen/ codegen — Rust binary (crate `mizan-codegen`); reads KDL IR,
|
||||
emits typed clients. Targets: stage1, react, vue, svelte,
|
||||
channels, python, rust. Askama templates under templates/.
|
||||
mizan-generate/ thin npm-package launcher (bin/launcher.mjs) dispatching to
|
||||
the compiled mizan-codegen binary per platform
|
||||
workers/ runtime workers / bridges
|
||||
mizan-ssr/ Bun subprocess used by the Django template backend
|
||||
```
|
||||
|
||||
## Two orthogonal products
|
||||
|
||||
- **RPC** — typed client generation via codegen
|
||||
- **SSR** — server rendering via the Bun bridge
|
||||
- **SSR** — server rendering via the embedded-V8 engine in the Rust
|
||||
binary (see `docs/SSR_ARCHITECTURE.md`)
|
||||
|
||||
Independent and composable. Either ships standalone; together they
|
||||
compose.
|
||||
@@ -68,9 +69,9 @@ contexts, types, and invalidation graph. Every codegen target consumes
|
||||
KDL. KDL is the contract; everything else (REST envelopes, OpenAPI
|
||||
documents, framework idioms) is sediment around it.
|
||||
|
||||
The IR must be validated against multiple adapters before it is
|
||||
considered stable. Single-adapter validation hides assumptions —
|
||||
divergence between adapters is what the IR exists to prevent.
|
||||
The IR is validated against multiple adapters — single-adapter
|
||||
validation hides assumptions, and divergence between adapters is what
|
||||
the IR exists to prevent.
|
||||
|
||||
Forward-direction primitives:
|
||||
|
||||
@@ -85,17 +86,16 @@ Forward-direction primitives:
|
||||
- `protocol/mizan-codegen/src/fetch.rs` spawns the configured source
|
||||
command and parses the KDL it writes.
|
||||
- Codegen reads KDL directly — no OpenAPI envelope, no
|
||||
`openapi-typescript`, no per-backend converter divergence. The
|
||||
former JavaScript/Node two-stage codegen (`openapi-typescript` plus
|
||||
`.mjs` adapters) has been deleted; codegen is now the single Rust
|
||||
binary.
|
||||
`openapi-typescript`, no per-backend converter divergence; codegen
|
||||
is a single Rust binary.
|
||||
- Edge manifest, MWT claims, and other protocol artifacts derive from
|
||||
the same registry/IR.
|
||||
|
||||
## Launch surface
|
||||
## Authoring surface
|
||||
|
||||
Python (Django) + React. Vue and Svelte ship as v1 alongside React.
|
||||
TypeScript backend (`mizan-ts`) proves the protocol is portable.
|
||||
Python (Django) + React is the reference stack. Vue and Svelte are
|
||||
co-equal codegen targets over the same kernel. The TypeScript backend
|
||||
(`mizan-ts`) proves the protocol is portable.
|
||||
|
||||
## Why the AFI shape
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# SSR Architecture
|
||||
|
||||
*Decided 2026-04-07.*
|
||||
|
||||
Mizan's SSR adapter is a **Django template backend**. It plugs into
|
||||
Django's existing `TEMPLATES` setting, replacing the template
|
||||
rendering engine.
|
||||
Mizan's SSR adapter is a **Django template backend** rendering through an
|
||||
**embedded V8 engine inside the Mizan Rust binary** (`cores/mizan-rust-ssr`).
|
||||
No external JS runtime serves requests — node and bun are build-time tools
|
||||
only, and no frontend adapter imports an SSR runtime or meta-framework.
|
||||
|
||||
```python
|
||||
TEMPLATES = [
|
||||
@@ -12,53 +11,74 @@ TEMPLATES = [
|
||||
'BACKEND': 'mizan.ssr.MizanTemplates',
|
||||
'DIRS': [BASE_DIR / 'frontend'],
|
||||
'OPTIONS': {
|
||||
'worker': 'path/to/mizan-ssr/src/worker.tsx',
|
||||
'bundles': BASE_DIR / 'frontend' / '.mizan' / 'ssr',
|
||||
'timeout': 5,
|
||||
},
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Then `render(request, 'components/Hello.tsx', context)` calls the Bun
|
||||
subprocess bridge instead of rendering a Django/Jinja2 template.
|
||||
**The template name IS a `.tsx`/`.jsx` file path**, resolved against
|
||||
`DIRS`; `get_template` returns a `MizanTemplate` wrapping the absolute
|
||||
file path. The context dict becomes the component's props (`request`
|
||||
and `csrf_token` stripped). Rendered output is wrapped in
|
||||
`<div id="mizan-root">…</div>` plus a
|
||||
`<script>window.__MIZAN_SSR_DATA__=…</script>` hydration payload.
|
||||
`render(request, 'components/Hello.tsx', context)` renders a React component
|
||||
instead of a Django/Jinja2 template. **The template name IS a `.tsx`/`.jsx`
|
||||
file path**, resolved against `DIRS`; `get_template` returns a
|
||||
`MizanTemplate` wrapping the resolved component. The context dict becomes
|
||||
the component's props (`request` and `csrf_token` stripped). Rendered output
|
||||
is wrapped in `<div id="mizan-root">…</div>` plus a
|
||||
`<script>window.__MIZAN_SSR_DATA__={sorted-json}</script>` hydration
|
||||
payload the Mizan kernel hydrates from — server-validated data crossing
|
||||
one way, as props.
|
||||
|
||||
## The engine
|
||||
|
||||
`SsrEngine` (`cores/mizan-rust-ssr`) embeds a `deno_core` V8 runtime
|
||||
composed with `deno_web`, so the web-platform globals react-dom touches
|
||||
(`TextEncoder`/`TextDecoder`, `MessagePort`, timers) are real
|
||||
implementations, not shims — a partial polyfill is silent-failure-shaped;
|
||||
it passes until a render path hits the gap.
|
||||
|
||||
- **The bundle is built, the engine evals it once.** `mizan-generate`'s SSR
|
||||
bundling step compiles each SSR entry component together with
|
||||
`react-dom/server.browser` into a self-contained bundle that assigns
|
||||
`globalThis.renderApp`. The engine evals the trusted bundle at
|
||||
construction and calls `renderApp(props)` per request.
|
||||
- **Props never enter evaluated source.** Per-render data crosses as a
|
||||
`v8::json::parse`d value passed as a function argument, so a prop string
|
||||
has no source to break out of — code injection is structurally absent,
|
||||
not filtered.
|
||||
- **One isolate per engine, one engine per worker thread.** V8's Locker
|
||||
constraint means an engine is not `Send`; the Django side holds one
|
||||
engine per (worker thread, bundle) pair.
|
||||
|
||||
## AFI boundary
|
||||
|
||||
| Side | Responsibility |
|
||||
|---|---|
|
||||
| Backend adapter (`SSRBridge`) | Manages the Bun subprocess lifecycle; gathers props |
|
||||
| Bun worker (`worker.tsx`) | `import()`s the file path, `renderToString(createElement(Component, props))` |
|
||||
| stdin/stdout JSON-RPC | Newline-delimited; `{id, method:"render", params:{file, props}}` → `{id, html}` / `{id, error}`; `ping` → `{id, pong:true}` |
|
||||
| Backend adapter (`mizan/ssr`) | Django template-backend interface; resolves the component path to its built bundle; gathers props; wraps output for hydration |
|
||||
| Rust binary (`SsrEngine`, via PyO3) | Holds the evaled bundle; parses props to a V8 value; `renderApp(props)` → HTML string |
|
||||
| Build step (`mizan-generate`) | Bundles component + `react-dom/server.browser` into the `bundles` directory; the only place node/bun run |
|
||||
|
||||
The Python side binds the engine through PyO3 — in-process FFI, no
|
||||
subprocess, no JSON-RPC framing. This is the Core-Consolidation shape:
|
||||
behavior lives in the binary; languages bind to it.
|
||||
|
||||
## No framework runtimes
|
||||
|
||||
No frontend adapter imports an SSR runtime or meta-framework — Next, Nuxt,
|
||||
SvelteKit, React Server Components / Flight. Those add the
|
||||
client-payload-deserialization step behind the pre-auth RCE class
|
||||
(CVE-2025-55182); the AFI already provides the typed, Pydantic-validated,
|
||||
one-way version they would otherwise import unsafely.
|
||||
|
||||
The guarantee is enforced, not assumed: `cores/mizan-rust-ssr/tests/no_rsc.rs`
|
||||
scans the authored SSR source and its dependencies for the forbidden token
|
||||
set (`react-server-dom`, `renderToReadableStream`, `renderToPipeableStream`,
|
||||
`createFromReadableStream`/`Fetch`, `use server`, `next/`, `nuxt`,
|
||||
`@sveltejs/kit`) and fails on presence — re-entry is loud.
|
||||
|
||||
## Why template backend
|
||||
|
||||
- Django's template system is swappable by design (batteries
|
||||
included, but replaceable).
|
||||
- Django developers already use `render(request, template, context)`
|
||||
— no new API to learn.
|
||||
- Django's template system is swappable by design (batteries included, but
|
||||
replaceable).
|
||||
- Django developers already use `render(request, template, context)` — no
|
||||
new API to learn.
|
||||
- URL routing, views, middleware, auth — all unchanged.
|
||||
|
||||
> A `templatetags/` package exists for a future `{% mizan_render %}`
|
||||
> convenience tag (base.html shell with Mizan components inside), but
|
||||
> it is currently empty — no tag is implemented yet.
|
||||
|
||||
## Implementation surface
|
||||
|
||||
The SSR backend (`mizan/ssr/backend.py`) implements Django's template
|
||||
backend interface:
|
||||
|
||||
- `MizanTemplates(BaseEngine)` — requires `OPTIONS['worker']` (path to
|
||||
`worker.tsx`); `get_template(name)` resolves a file under `DIRS`
|
||||
- `MizanTemplate` with `.render(context, request)` → calls the bridge
|
||||
- `SSRBridge` (`bridge.py`) — spawns `bun run <worker>`, holds the
|
||||
persistent subprocess, correlates requests by message id, thread-safe,
|
||||
auto-restarts on crash, waits for the worker's ready signal
|
||||
|
||||
Everything Django expects from a template backend, but the actual
|
||||
rendering routes to Bun.
|
||||
|
||||
Reference in New Issue
Block a user