Docs carry the shape of the system, not its status
MIZAN.md's phase-numbered implementation order, PRODUCT_ARCHITECTURE's "Deferred until Render revenue funds it" and "Shipped in", PSR_VS_EDGE's current-state section, and the READMEs' passing-test counts were all reporting where the work stood rather than what the system is. OWED_SURFACE keeps its subject — surface that is specified but unbuilt — stated as the shape each unit owes. The channel sections follow the renamed slots: Params / ClientMessage / ServerMessage, and Channel as the base class on both backends. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -119,18 +119,24 @@ dedicated `mizan-allauth` repository, built on this mixin.
|
||||
|
||||
## Channels
|
||||
|
||||
WebSocket-native RPC via a flag flip:
|
||||
WebSocket-native RPC via a flag flip. The message slots are named from the
|
||||
client's point of view: `ClientMessage` travels client → server,
|
||||
`ServerMessage` travels server → client. Declare only the directions the
|
||||
channel uses.
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from mizan.channels import ReactChannel
|
||||
from mizan.channels import Channel
|
||||
|
||||
|
||||
class ChatChannel(ReactChannel):
|
||||
class ChatChannel(Channel):
|
||||
class Params(BaseModel):
|
||||
room: str
|
||||
|
||||
class DjangoMessage(BaseModel):
|
||||
class ClientMessage(BaseModel):
|
||||
text: str
|
||||
|
||||
class ServerMessage(BaseModel):
|
||||
text: str
|
||||
user: str
|
||||
|
||||
@@ -139,10 +145,19 @@ class ChatChannel(ReactChannel):
|
||||
|
||||
def group(self, params):
|
||||
return f"chat_{params.room}"
|
||||
|
||||
def receive(self, params, msg):
|
||||
return self.ServerMessage(text=msg.text, user=self.user.email)
|
||||
```
|
||||
|
||||
Frontend gets `useChatChannel({ room })`.
|
||||
|
||||
Server code outside a subscription broadcasts with `push()`:
|
||||
|
||||
```python
|
||||
await ChatChannel.push(room="general", message=ChatChannel.ServerMessage(...))
|
||||
```
|
||||
|
||||
## Generate the frontend
|
||||
|
||||
The codegen is the `mizan-generate` Rust binary (source at
|
||||
|
||||
@@ -5,11 +5,10 @@ function. Typed React client generated. Invalidation automatic.
|
||||
|
||||
## Scope
|
||||
|
||||
mizan-fastapi targets the **AFI-common subset** — RPC dispatch, context
|
||||
bundling, JSON-body invalidation, and auth gating. Forms, Channels, Shapes,
|
||||
and SSR are out of scope for the FastAPI adapter — FastAPI projects use
|
||||
native equivalents (Pydantic, native WebSockets, ORM-of-choice, FastAPI's
|
||||
own SSR ecosystem).
|
||||
mizan-fastapi's surface is RPC dispatch, context bundling, JSON-body
|
||||
invalidation, auth gating, and channels over a multiplexed WebSocket. Forms,
|
||||
Shapes, and SSR sit outside that surface — a FastAPI project reaches for its
|
||||
own native equivalents (Pydantic, ORM-of-choice, FastAPI's SSR ecosystem).
|
||||
|
||||
## Install
|
||||
|
||||
@@ -29,11 +28,13 @@ from mizan_fastapi import (
|
||||
mizan_exception_handler,
|
||||
mizan_validation_handler,
|
||||
router as mizan_router,
|
||||
ws_router,
|
||||
)
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(mizan_router, prefix="/api/mizan")
|
||||
app.include_router(ws_router, prefix="/api/mizan")
|
||||
app.add_exception_handler(MizanError, mizan_exception_handler)
|
||||
app.add_exception_handler(RequestValidationError, mizan_validation_handler)
|
||||
```
|
||||
@@ -82,9 +83,56 @@ a dedicated `clients.py` imported during startup.
|
||||
@client(rev=2) # cache revision (busts on bump)
|
||||
```
|
||||
|
||||
`websocket=True`, Forms, and Channels parameters are accepted by the
|
||||
decorator (they're a `mizan-core` primitive) but ignored by mizan-fastapi —
|
||||
those features only have effect when paired with mizan-django.
|
||||
Forms parameters are accepted by the decorator (they're a `mizan-core`
|
||||
primitive) and carry no meaning to this adapter.
|
||||
|
||||
## Channels
|
||||
|
||||
A channel is a named fan-out over the WebSocket `ws_router` serves. Subclass
|
||||
`Channel`, declare whichever payload models the channel carries, and register
|
||||
it. The three model names are read from the client's side: `Params` keys the
|
||||
fan-out, `ClientMessage` travels up, `ServerMessage` travels down.
|
||||
|
||||
```python
|
||||
from mizan_fastapi import Channel, register_channel
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Chat(Channel):
|
||||
|
||||
class Params(BaseModel):
|
||||
room: str
|
||||
|
||||
class ClientMessage(BaseModel):
|
||||
text: str
|
||||
|
||||
class ServerMessage(BaseModel):
|
||||
user: str
|
||||
text: str
|
||||
|
||||
def authorize(self, params: Params) -> bool:
|
||||
return True
|
||||
|
||||
def receive(self, params: Params, msg: ClientMessage) -> ServerMessage:
|
||||
return self.ServerMessage(user="anon", text=msg.text)
|
||||
|
||||
|
||||
register_channel(Chat, "chat")
|
||||
```
|
||||
|
||||
Server code pushes to a group from anywhere:
|
||||
|
||||
```python
|
||||
await Chat.push(Chat.ServerMessage(user="system", text="hello"), room="general")
|
||||
```
|
||||
|
||||
Registered channels contribute to the exported IR, so codegen emits the
|
||||
`<Pascal>Params` / `<Pascal>ClientMessage` / `<Pascal>ServerMessage` types and
|
||||
the matching frontend hook.
|
||||
|
||||
Group membership lives in the process that holds the socket, so a push reaches
|
||||
only the subscribers attached to that process. Fan-out that spans processes is
|
||||
a shared broker in front of `broadcast`.
|
||||
|
||||
## Auth integration
|
||||
|
||||
@@ -169,8 +217,6 @@ python -m mizan_fastapi.ir <module>
|
||||
|
||||
Imports the named module (which must register every `@client` function as
|
||||
import-time side effects), then prints the Mizan KDL IR to stdout.
|
||||
Mirrors mizan-django's `manage.py export_mizan_ir` so the codegen consumes
|
||||
either backend the same subprocess way.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -183,4 +229,4 @@ emit equivalent schemas for the same registered functions. See
|
||||
|
||||
A live e2e harness exercises this adapter end-to-end at
|
||||
`examples/fastapi-react-site/` (real Chromium → React with generated hooks
|
||||
→ FastAPI server, 14/14 Playwright tests).
|
||||
→ FastAPI server, driven by Playwright).
|
||||
|
||||
@@ -215,11 +215,11 @@ const greeting = await callGreet({ name: "world" });
|
||||
console.log(greeting.message);
|
||||
```
|
||||
|
||||
For framework hooks generated by Stage 2 (`useGreet()` etc., wrapping the
|
||||
imperative `callGreet` with `isPending`/`error` state), wrap your tree
|
||||
with `<MizanContext>` at the root — same as the HTTP-transport setup. The
|
||||
generated provider is transport-agnostic; it reads from `config.transport`
|
||||
the kernel is using.
|
||||
For the framework hooks the `react` target generates (`useGreet()` etc.,
|
||||
wrapping the imperative `callGreet` with `isPending`/`error` state), wrap
|
||||
your tree with `<MizanContext>` at the root — same as the HTTP-transport
|
||||
setup. The generated provider is transport-agnostic; it reads from
|
||||
`config.transport` the kernel is using.
|
||||
|
||||
### tsconfig / vite preserve symlinks
|
||||
|
||||
@@ -273,12 +273,6 @@ Errors flow through Tauri's `Promise.reject` path; `@mizan/tauri-transport`
|
||||
re-wraps them into the same `MizanError` shape the HTTP transport
|
||||
produces, so consumer code is identical regardless of transport.
|
||||
|
||||
## Reference application
|
||||
|
||||
`claude-manage` is the production reference — Tauri + React + Pydantic
|
||||
schema + Mizan RPC. See `~/dev/claude-manage/mizan.toml` and
|
||||
`~/dev/claude-manage/src-tauri/src/commands.rs` for a full migrated app.
|
||||
|
||||
## Architecture
|
||||
|
||||
mizan-tauri shares `cores/mizan-rust` with `mizan-rust-axum`. Both
|
||||
|
||||
Reference in New Issue
Block a user