Add CDN-ready headers, ROADMAP, fold runtime into mizan-react
CDN headers on context GETs (Edge-ready): - Cache-Control: public, max-age=0, stale-while-revalidate=300 - Vary: Authorization, Cookie - Deterministic JSON (sorted keys) for consistent cache keys - Error responses: Cache-Control: no-store - Mutation POSTs: Cache-Control: no-store ROADMAP.md documents v1 deliverables and Mizan Cloud (Edge, Render, Deploy) as closed-source products built on the open-source protocol. mizan-runtime folded into mizan-react/src/runtime/ — framework-agnostic split deferred until a second frontend adapter exists. 268 Django + 33 React tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
122
ROADMAP.md
Normal file
122
ROADMAP.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Mizan Roadmap
|
||||||
|
|
||||||
|
## v1 — Django + React
|
||||||
|
|
||||||
|
### Done
|
||||||
|
|
||||||
|
- **@client decorator** — `context=`, `affects=`, `auth=`, `websocket=`
|
||||||
|
- **ReactContext class** — type-safe context/affects references with linting
|
||||||
|
- **Named contexts** — functions sharing a context name are grouped into one provider and one fetch
|
||||||
|
- **Context bundling endpoint** — `GET /api/mizan/ctx/<name>/` returns all functions in one response
|
||||||
|
- **Server-driven invalidation** — mutation responses carry `invalidate` from `affects=` metadata
|
||||||
|
- **Scoped invalidation** — runtime supports `invalidate: [{context: "user", params: {user_id: 5}}]`
|
||||||
|
- **Param elevation** — shared params become required provider props, non-shared become optional
|
||||||
|
- **Schema export** — `x-mizan-functions` + `x-mizan-contexts` for codegen
|
||||||
|
- **Auth guards** — `auth=True`, `auth='staff'`, `auth='superuser'`, `auth=callable`
|
||||||
|
- **JWT + session auth** — auto-detected, CSRF handled
|
||||||
|
- **Shapes** — Pydantic + django-readers for typed query projections
|
||||||
|
- **WebSocket channels** — real-time bidirectional communication
|
||||||
|
- **Codegen** — generates typed React providers, hooks, mutations from schema
|
||||||
|
|
||||||
|
### Next: CDN-Ready Headers
|
||||||
|
|
||||||
|
Context GETs must be cacheable by a CDN sitting in front of Django.
|
||||||
|
|
||||||
|
- `Cache-Control` headers on `GET /api/mizan/ctx/<name>/` responses
|
||||||
|
- `Vary` headers for auth-dependent responses
|
||||||
|
- Deterministic JSON output (sorted keys)
|
||||||
|
- Mutation POSTs must return `Cache-Control: no-store`
|
||||||
|
- Context error responses must not be cached
|
||||||
|
|
||||||
|
### Next: Fold mizan-runtime into mizan-react
|
||||||
|
|
||||||
|
The runtime (~150 lines: context registry, invalidation batcher, fetch primitives) merges into mizan-react. Framework-agnostic split is deferred until a second frontend adapter exists.
|
||||||
|
|
||||||
|
### Next: SSR Bridge
|
||||||
|
|
||||||
|
Django renders React components server-side via a persistent Bun subprocess.
|
||||||
|
|
||||||
|
- Bun worker: stdin/stdout JSON-RPC, `renderToString`, component registry
|
||||||
|
- Django bridge: subprocess management, IPC, request synthesis
|
||||||
|
- Template tag: `{% mizan_render "ProfilePage" user_profile=profile %}`
|
||||||
|
- Hydration: `window.__MIZAN_SSR_DATA__` consumed by generated providers
|
||||||
|
- Generated contexts check SSR data before first fetch
|
||||||
|
|
||||||
|
### Next: Codegen Rewrite
|
||||||
|
|
||||||
|
Generated code uses the runtime directly (`mizanFetch`, `mizanCall`, `registerContext`) instead of the legacy `MizanProvider` pattern. Mutations have zero invalidation knowledge — the runtime reads the server response.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mizan Cloud (closed-source)
|
||||||
|
|
||||||
|
### Mizan Edge
|
||||||
|
|
||||||
|
Cloudflare Workers for automatic edge caching.
|
||||||
|
|
||||||
|
- Reads the Mizan schema to configure cache rules
|
||||||
|
- Context GETs cached at the edge, keyed by context name + params
|
||||||
|
- Mutation POSTs trigger cache purges from the `invalidate` response key
|
||||||
|
- Zero configuration — the schema IS the cache policy
|
||||||
|
|
||||||
|
### Mizan Render
|
||||||
|
|
||||||
|
SSR at the edge via Cloudflare Workers.
|
||||||
|
|
||||||
|
- The Bun SSR bridge, running on Cloudflare instead of colocated with Django
|
||||||
|
- Context data fetched from Django (or edge cache), rendered at the edge
|
||||||
|
- HTML response streamed to the user from the nearest PoP
|
||||||
|
|
||||||
|
### Mizan Deploy
|
||||||
|
|
||||||
|
One-command deployment for Django + React apps.
|
||||||
|
|
||||||
|
- Container orchestration (AWS/Azure)
|
||||||
|
- Edge + Render auto-configured
|
||||||
|
- `mizan deploy` from the CLI
|
||||||
|
- The Vercel experience for Django
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Spec
|
||||||
|
|
||||||
|
The protocol is the product. Every endpoint is designed for a CDN.
|
||||||
|
|
||||||
|
### Context fetch
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/mizan/ctx/<name>/?param=value
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
Cache-Control: public, max-age=60, stale-while-revalidate=300
|
||||||
|
Vary: Authorization, Cookie
|
||||||
|
|
||||||
|
{
|
||||||
|
"function_a": { ... },
|
||||||
|
"function_b": [ ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mutation call
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/mizan/call/
|
||||||
|
Cache-Control: no-store
|
||||||
|
|
||||||
|
{
|
||||||
|
"result": { ... },
|
||||||
|
"invalidate": ["context_name"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scoped invalidation
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"result": { ... },
|
||||||
|
"invalidate": [
|
||||||
|
"notifications",
|
||||||
|
{ "context": "user", "params": { "user_id": 5 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -496,7 +496,9 @@ def function_call_view(request: HttpRequest) -> JsonResponse:
|
|||||||
# Dedupe while preserving order
|
# Dedupe while preserving order
|
||||||
response_data["invalidate"] = list(dict.fromkeys(invalidate))
|
response_data["invalidate"] = list(dict.fromkeys(invalidate))
|
||||||
|
|
||||||
return JsonResponse(response_data)
|
response = JsonResponse(response_data)
|
||||||
|
response["Cache-Control"] = "no-store"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def execute_context(
|
def execute_context(
|
||||||
@@ -578,11 +580,15 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse:
|
|||||||
|
|
||||||
Endpoint: GET /api/mizan/ctx/<context_name>/?param1=val1¶m2=val2
|
Endpoint: GET /api/mizan/ctx/<context_name>/?param1=val1¶m2=val2
|
||||||
|
|
||||||
Response: raw bundled data (keys are function names, values are results)
|
Response: raw bundled data, CDN-cacheable.
|
||||||
{
|
{
|
||||||
"user_profile": { ... },
|
"user_profile": { ... },
|
||||||
"user_orders": [ ... ]
|
"user_orders": [ ... ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Headers:
|
||||||
|
Cache-Control: public, max-age=0, stale-while-revalidate=300
|
||||||
|
Vary: Authorization, Cookie
|
||||||
"""
|
"""
|
||||||
if request.method != "GET":
|
if request.method != "GET":
|
||||||
return FunctionError(
|
return FunctionError(
|
||||||
@@ -603,7 +609,18 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse:
|
|||||||
ErrorCode.INTERNAL_ERROR: 500,
|
ErrorCode.INTERNAL_ERROR: 500,
|
||||||
ErrorCode.NOT_IMPLEMENTED: 501,
|
ErrorCode.NOT_IMPLEMENTED: 501,
|
||||||
}.get(result.code, 400)
|
}.get(result.code, 400)
|
||||||
return result.to_response(status=status)
|
error_response = result.to_response(status=status)
|
||||||
|
error_response["Cache-Control"] = "no-store"
|
||||||
|
return error_response
|
||||||
|
|
||||||
# Return raw bundled data (not wrapped in {"error": false, "data": ...})
|
# Deterministic JSON (sorted keys) for consistent cache keys
|
||||||
return JsonResponse(result.data)
|
response = JsonResponse(result.data, json_dumps_params={"sort_keys": True})
|
||||||
|
|
||||||
|
# CDN-ready headers
|
||||||
|
# max-age=0: browser always revalidates (mutations may have invalidated)
|
||||||
|
# stale-while-revalidate: edge can serve stale while fetching fresh
|
||||||
|
# Vary: different auth = different cache entry
|
||||||
|
response["Cache-Control"] = "public, max-age=0, stale-while-revalidate=300"
|
||||||
|
response["Vary"] = "Authorization, Cookie"
|
||||||
|
|
||||||
|
return response
|
||||||
|
|||||||
@@ -797,6 +797,7 @@ class ServerDrivenInvalidationTests(TestCase):
|
|||||||
self.assertIn("result", data)
|
self.assertIn("result", data)
|
||||||
self.assertIn("invalidate", data)
|
self.assertIn("invalidate", data)
|
||||||
self.assertEqual(data["invalidate"], ["user"])
|
self.assertEqual(data["invalidate"], ["user"])
|
||||||
|
self.assertEqual(response["Cache-Control"], "no-store")
|
||||||
|
|
||||||
def test_mutation_without_affects_has_no_invalidate(self):
|
def test_mutation_without_affects_has_no_invalidate(self):
|
||||||
"""Mutation without affects= returns result only."""
|
"""Mutation without affects= returns result only."""
|
||||||
@@ -846,6 +847,23 @@ class ServerDrivenInvalidationTests(TestCase):
|
|||||||
self.assertIn("team_info", data)
|
self.assertIn("team_info", data)
|
||||||
self.assertEqual(data["team_info"]["name"], "team_3")
|
self.assertEqual(data["team_info"]["name"], "team_3")
|
||||||
|
|
||||||
|
# CDN-ready headers
|
||||||
|
self.assertIn("public", response["Cache-Control"])
|
||||||
|
self.assertIn("stale-while-revalidate", response["Cache-Control"])
|
||||||
|
self.assertIn("Authorization", response["Vary"])
|
||||||
|
self.assertIn("Cookie", response["Vary"])
|
||||||
|
|
||||||
|
def test_context_error_not_cached(self):
|
||||||
|
"""Context fetch errors must not be cached."""
|
||||||
|
from mizan.client.executor import context_fetch_view
|
||||||
|
|
||||||
|
request = self.factory.get("/api/mizan/ctx/nonexistent/")
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
|
||||||
|
response = context_fetch_view(request, "nonexistent")
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertEqual(response["Cache-Control"], "no-store")
|
||||||
|
|
||||||
|
|
||||||
class ContextFetchTests(TestCase):
|
class ContextFetchTests(TestCase):
|
||||||
"""Tests for the bundled context fetch endpoint (execute_context)."""
|
"""Tests for the bundled context fetch endpoint (execute_context)."""
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@mizan/runtime",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Mizan client state engine — framework-agnostic context registry, invalidation, and fetch.",
|
|
||||||
"type": "module",
|
|
||||||
"main": "index.ts",
|
|
||||||
"types": "index.ts",
|
|
||||||
"exports": {
|
|
||||||
".": "./index.ts"
|
|
||||||
},
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user