From f4d7c64e3c8910919ba138a6bfb4fcef5bc1348a Mon Sep 17 00:00:00 2001 From: Ryth Azhur Date: Thu, 2 Apr 2026 17:53:31 -0400 Subject: [PATCH] Add CDN-ready headers, ROADMAP, fold runtime into mizan-react MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ROADMAP.md | 122 ++++++++++++++++++ .../mizan-django/src/mizan/client/executor.py | 27 +++- .../mizan-django/src/mizan/tests/test_core.py | 18 +++ .../src/runtime}/index.ts | 0 packages/mizan-runtime/package.json | 12 -- 5 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 ROADMAP.md rename packages/{mizan-runtime => mizan-react/src/runtime}/index.ts (100%) delete mode 100644 packages/mizan-runtime/package.json diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..8036652 --- /dev/null +++ b/ROADMAP.md @@ -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//` 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//` 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//?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 } } + ] +} +``` diff --git a/packages/mizan-django/src/mizan/client/executor.py b/packages/mizan-django/src/mizan/client/executor.py index 1896d71..eb5108f 100644 --- a/packages/mizan-django/src/mizan/client/executor.py +++ b/packages/mizan-django/src/mizan/client/executor.py @@ -496,7 +496,9 @@ def function_call_view(request: HttpRequest) -> JsonResponse: # Dedupe while preserving order 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( @@ -578,11 +580,15 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse: Endpoint: GET /api/mizan/ctx//?param1=val1¶m2=val2 - Response: raw bundled data (keys are function names, values are results) + Response: raw bundled data, CDN-cacheable. { "user_profile": { ... }, "user_orders": [ ... ] } + + Headers: + Cache-Control: public, max-age=0, stale-while-revalidate=300 + Vary: Authorization, Cookie """ if request.method != "GET": return FunctionError( @@ -603,7 +609,18 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse: ErrorCode.INTERNAL_ERROR: 500, ErrorCode.NOT_IMPLEMENTED: 501, }.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": ...}) - return JsonResponse(result.data) + # Deterministic JSON (sorted keys) for consistent cache keys + 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 diff --git a/packages/mizan-django/src/mizan/tests/test_core.py b/packages/mizan-django/src/mizan/tests/test_core.py index 10f19a2..f9a0d6a 100644 --- a/packages/mizan-django/src/mizan/tests/test_core.py +++ b/packages/mizan-django/src/mizan/tests/test_core.py @@ -797,6 +797,7 @@ class ServerDrivenInvalidationTests(TestCase): self.assertIn("result", data) self.assertIn("invalidate", data) self.assertEqual(data["invalidate"], ["user"]) + self.assertEqual(response["Cache-Control"], "no-store") def test_mutation_without_affects_has_no_invalidate(self): """Mutation without affects= returns result only.""" @@ -846,6 +847,23 @@ class ServerDrivenInvalidationTests(TestCase): self.assertIn("team_info", data) 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): """Tests for the bundled context fetch endpoint (execute_context).""" diff --git a/packages/mizan-runtime/index.ts b/packages/mizan-react/src/runtime/index.ts similarity index 100% rename from packages/mizan-runtime/index.ts rename to packages/mizan-react/src/runtime/index.ts diff --git a/packages/mizan-runtime/package.json b/packages/mizan-runtime/package.json deleted file mode 100644 index 22baaf5..0000000 --- a/packages/mizan-runtime/package.json +++ /dev/null @@ -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" -}