Cleaned dead code and updated documents
This commit is contained in:
41
CLAUDE.md
41
CLAUDE.md
@@ -17,15 +17,24 @@ backends/ server protocol adapters
|
|||||||
mizan-django/ Django adapter
|
mizan-django/ Django adapter
|
||||||
mizan-fastapi/ FastAPI adapter (RPC + context + invalidation; AFI-common scope)
|
mizan-fastapi/ FastAPI adapter (RPC + context + invalidation; AFI-common scope)
|
||||||
mizan-ts/ TypeScript adapter (proves the protocol is language-agnostic)
|
mizan-ts/ TypeScript adapter (proves the protocol is language-agnostic)
|
||||||
frontends/ client kernel + per-framework adapters
|
mizan-rust-axum/ Rust/Axum adapter (server-side substrate; three-way parity)
|
||||||
|
mizan-tauri/ Tauri-as-Mizan-backend substrate
|
||||||
|
frontends/ client kernel + per-framework adapters + transports
|
||||||
mizan-base/ framework-agnostic kernel; owns data, status, error; adapters subscribe
|
mizan-base/ framework-agnostic kernel; owns data, status, error; adapters subscribe
|
||||||
mizan-react/ React contexts + hooks over the kernel
|
mizan-react/ React contexts + hooks over the kernel
|
||||||
mizan-vue/ Vue composables over the kernel
|
mizan-vue/ Vue composables over the kernel (codegen target; runtime package unimplemented)
|
||||||
mizan-svelte/ Svelte stores/runes over the kernel
|
mizan-svelte/ Svelte stores over the kernel (codegen target; runtime package unimplemented)
|
||||||
|
mizan-rust/ Rust kernel (PyO3 bridge; consumed by the Rust codegen's python target)
|
||||||
|
mizan-tauri-transport/ Tauri IPC transport for the kernel
|
||||||
|
mizan-webview-transport/ VSCode-webview transport for the kernel
|
||||||
|
mizan-webview-channels/ webview channel transport
|
||||||
cores/ shared language-level primitives
|
cores/ shared language-level primitives
|
||||||
mizan-python/ @client decorator, registry, MWT, HMAC cache keys; consumed by both Python backends
|
mizan-python/ @client decorator, registry, MWT, HMAC cache keys; consumed by both Python backends
|
||||||
|
mizan-rust/ shared Rust primitives (IR, KDL, registry, graph-check)
|
||||||
|
mizan-rust-macros/ proc-macros for the Rust backend/kernel
|
||||||
protocol/ protocol-level tooling
|
protocol/ protocol-level tooling
|
||||||
mizan-generate/ codegen — fetches schema from any backend, emits typed React/Vue/Svelte client
|
mizan-codegen/ the codegen — a Rust binary; reads KDL IR, emits typed clients per target
|
||||||
|
mizan-generate/ thin npm launcher around the compiled mizan-codegen binary
|
||||||
workers/ runtime workers / bridges
|
workers/ runtime workers / bridges
|
||||||
mizan-ssr/ Bun subprocess used by the Django template backend
|
mizan-ssr/ Bun subprocess used by the Django template backend
|
||||||
```
|
```
|
||||||
@@ -446,22 +455,18 @@ urlpatterns = [
|
|||||||
|
|
||||||
## Codegen — Current State
|
## Codegen — Current State
|
||||||
|
|
||||||
The codegen is `protocol/mizan-generate/` — framework-agnostic, two-stage. Stage 1 emits the protocol layer (`callXxx` for mutations, `fetchXxx` for context bundles, types). Stage 2 emits per-framework hooks/composables/stores that subscribe to the `mizan-base` kernel.
|
The codegen is a **Rust binary**, `protocol/mizan-codegen/` (crate `mizan-codegen`). `protocol/mizan-generate/` is a thin npm launcher (`bin/launcher.mjs`) that shells out to the compiled binary. The IR is **KDL** — each backend emits KDL describing its functions/contexts; the binary reads it (`src/fetch.rs`, `src/ir.rs`) and emits per-target output from Askama templates (`templates/`, dispatched in `src/emit/`).
|
||||||
|
|
||||||
**What's in place:**
|
Two layers, same as before: a framework-agnostic protocol layer (`callXxx` for mutations, `fetchXxx` for context bundles, types) and a per-framework adapter layer that subscribes to the `mizan-base` kernel.
|
||||||
|
|
||||||
- Function hooks (`useEcho`, `useUserProfile`, etc.) in the React adapter, subscribing to kernel state via `useSyncExternalStore`
|
**Targets** (`src/emit/`, each byte-checked by a `*_parity.rs` test):
|
||||||
- Context hooks for named contexts and `global`
|
|
||||||
- Channel hooks for WebSocket transport
|
|
||||||
- Vue and Svelte equivalents (Stage 2 templates compile, but no live-backend example exercises them — see `ISSUES.md` A4)
|
|
||||||
|
|
||||||
**What's not yet emitted (the wrapper layer):**
|
- `react` — function/context hooks over `useSyncExternalStore`, plus the full wrapper layer: the `MizanContext` root provider (calls `configure()`, mounts the global context), `useMizan()` imperative escape hatch, and `useMutation`-backed hooks exposing `{ mutate, isPending, error }`.
|
||||||
|
- `vue`, `svelte` — composables / `readable` stores. Byte-parity-tested, but no runtime adapter package or live-backend example exercises them yet (the `mizan-vue`/`mizan-svelte` packages are unimplemented stubs).
|
||||||
|
- `channels` — WebSocket transport hooks.
|
||||||
|
- `stage1` — the framework-agnostic protocol files.
|
||||||
|
- `python`, `rust` — typed clients for the Python (PyO3) and Rust frontends.
|
||||||
|
|
||||||
- `<MizanContext>` provider component for React (calls `configure()` and mounts the kernel into the component tree)
|
The pre-kernel `MizanProvider` in `mizan-react/src/context.tsx` (~750 lines) still ships and is imported by the desktop example; it coexists with the generated `MizanContext`. Forms (`mizan-react/src/forms.ts`) are hand-written and consume the pre-kernel provider — a form codegen target wired to `mizanCall` is still owed. See `ISSUES.md`.
|
||||||
- `useMizan()` hook for accessing the kernel from React
|
|
||||||
- Framework-named error class (e.g. `DjangoError`) wrapping `MizanError` from the kernel
|
|
||||||
- Vue and Svelte equivalents
|
|
||||||
|
|
||||||
The legacy `MizanProvider` in `mizan-react/src/context.tsx` (~750 lines) and the `forms.ts` consumer (~1163 lines) still depend on the pre-kernel API. They're tracked as `ISSUES.md` A1 and A3. Removing them is gated on the wrapper layer above being emitted.
|
The SSR pipeline is independent of the codegen — the Bun worker resolves a component by **file path** (`import(file)` + `renderToString`), not via a registry.
|
||||||
|
|
||||||
The SSR pipeline is independent of the codegen — it renders whatever components are registered in the Bun worker.
|
|
||||||
|
|||||||
189
ISSUES.md
189
ISSUES.md
@@ -1,178 +1,23 @@
|
|||||||
# Mizan — Known Issues
|
# Mizan — Known Issues
|
||||||
|
|
||||||
Identified by domain expert review (Cloudflare, Serverless, Vercel, React Query, Django, Laravel, Vue/Svelte).
|
Status board against the current codebase: Rust codegen (`protocol/mizan-codegen`),
|
||||||
|
KDL IR, kernel-owned frontend state (`@mizan/base`). Issues that the earlier
|
||||||
|
expert-review board filed against the deleted JavaScript codegen and the
|
||||||
|
pre-kernel `mizan-react` provider have been removed — they audited files that
|
||||||
|
no longer exist.
|
||||||
|
|
||||||
## Fixed
|
## Open
|
||||||
|
|
||||||
- ~~C1~~ Scoped cache purge now passes user_id
|
- [ ] **Vue / Svelte frontend packages are unimplemented stubs.** `frontends/mizan-vue` and `frontends/mizan-svelte` contain only a `package.json` — no `src/`. The Rust codegen emits Vue composables and Svelte stores (`src/emit/vue.rs`, `src/emit/svelte.rs`, byte-checked by `vue_svelte_parity.rs`), but there is no runtime kernel-adapter package for either and no example app exercises them against a live backend. React is the only frontend with full integration verification.
|
||||||
- ~~C2~~ initSession retries 3x, resets on failure
|
- [ ] **Svelte adapter emits Svelte 4 stores.** `src/emit/svelte.rs` generates `readable` stores from `svelte/store`. Svelte 5 `$state`/`$derived` runes are the current idiom.
|
||||||
- ~~C3~~ SSR backend injects `__MIZAN_SSR_DATA__` script tag
|
- [ ] **Forms have no codegen target.** `mizan-react/src/forms.ts` (form core hooks) is hand-written and consumed via the pre-kernel `MizanProvider`; the e2e harness has its form fixtures removed. A form codegen target wired to `mizanCall` is owed.
|
||||||
- ~~C4~~ SSR bridge uses _write_lock for stdin
|
- [ ] **Pre-kernel MizanProvider still shipped.** `mizan-react/src/context.tsx` (~750 lines) is the pre-kernel provider, still imported by the desktop example. It coexists with the codegen-emitted `MizanContext` (which subscribes to `@mizan/base`). Migrating the desktop example onto the generated provider retires it.
|
||||||
- ~~C5~~ SSR bridge registers atexit handler
|
- [ ] **Cache module open issues.** See `backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md`: purge atomicity, cross-language stringification, per-param sub-index cleanup, thundering-herd protection, `cache_get`/`cache_put` argument inconsistency, RedisCache test coverage.
|
||||||
- ~~C7~~ View-path mutations now purge origin cache
|
- [ ] **Packages missing a README.** `frontends/mizan-base` (the kernel everything imports), `protocol/mizan-codegen` (the codegen binary), `frontends/mizan-vue`, `frontends/mizan-svelte`, `frontends/mizan-rust`, `backends/mizan-ts`, `backends/mizan-rust-axum`, `cores/mizan-python`.
|
||||||
- ~~H1~~ pendingScoped is Array, not Map (no overwrite)
|
|
||||||
- ~~H2~~ stableKey() sorts JSON keys (order-independent)
|
|
||||||
- ~~H3~~ mizanFetch retries 2x on 5xx/network errors
|
|
||||||
- ~~H4~~ Named contexts skip refetch if SSR data exists
|
|
||||||
- ~~H6~~ refreshContext uses GET /ctx/ not POST /call/
|
|
||||||
- ~~H10~~ _meta always fresh dict
|
|
||||||
- ~~H11~~ Python normalizes True→"true" for cross-language HMAC
|
|
||||||
- ~~H13~~ isValid checks all required fields are touched
|
|
||||||
- ~~H14~~ `@client(merge=...)` primitive — kernel splices return value into cached context, no refetch
|
|
||||||
- ~~H15~~ Schema export handles `BaseModel | None` return types
|
|
||||||
- ~~H16~~ Generated `react.tsx` imports per-function `*Output` types
|
|
||||||
- ~~M11~~ execute_function return type includes HttpResponseBase
|
|
||||||
- ~~M18~~ registerContext cleanup uses ?. (no crash)
|
|
||||||
- ~~M19~~ `list[BaseModel]` returns reach the wire as bare arrays (RootModel-based rename, no `{result: ...}` wrap)
|
|
||||||
- ~~M20~~ `initSession()` gated on `configure({session: bool})` (Django default-on, FastAPI opt-out)
|
|
||||||
|
|
||||||
## Remaining Critical
|
## Resolved this pass
|
||||||
|
|
||||||
### C6. No loading/error/stale states in runtime
|
- [x] **Codegen test suite compile break** — every `mizan-codegen` test constructed `SourceConfig` without the `rust`/`script` fields added alongside the Rust-backend work. Suite now compiles and is green.
|
||||||
**File:** `mizan-base/src/index.ts`
|
- [x] **React parity baseline** — the emitter correctly drops the dead `initSession`/`MizanError` top-level imports (they are only re-exported, never used in the module body); baseline regenerated. Fixed the template whitespace artifact that indented the `} from '@mizan/base'` closing brace.
|
||||||
The kernel stores only `{params, refetch}`. No `data`, `status`, `error`. Every adapter reinvents loading tracking. Blocks stale-while-revalidate.
|
- [x] **Edge manifest non-determinism** — `generate_edge_manifest` iterated registration order; now sorts context and mutation keys, so the manifest is deterministic regardless of registration order.
|
||||||
|
- [x] **Dead code removed** — `workers/mizan-ssr/src/test-worker.tsx` (a relic of the rejected `registerComponent` registry), unused TS helpers `isResponseReturn` and `sortedStringify` (mizan-ts), the unused `IndexMap` import (`emit/python.rs`), the dead `debug_expose_names` Django setting, and the dead `package.json` exports + vite aliases (`./client/nextjs`, `./allauth`, `./allauth/nextjs`) pointing at source that does not exist.
|
||||||
## Remaining High
|
|
||||||
|
|
||||||
### H5. Mutation hooks expose no loading/error state
|
|
||||||
**File:** `protocol/mizan-generate/generator/lib/adapters/react.mjs`
|
|
||||||
Returns bare `useCallback`. No `isPending`, `error`, `isSuccess`.
|
|
||||||
|
|
||||||
### H7. Redis SCAN blocks request path at scale
|
|
||||||
**File:** `mizan-django/src/mizan/cache/backend.py`
|
|
||||||
Synchronous SCAN at 1M keys: multi-second blocking.
|
|
||||||
|
|
||||||
### H8. Svelte codegen uses Svelte 4 stores
|
|
||||||
**File:** `protocol/mizan-generate/generator/lib/adapters/svelte.mjs`
|
|
||||||
Should use Svelte 5 `$state`/`$derived` runes.
|
|
||||||
|
|
||||||
### H9. Svelte destroy() not auto-called
|
|
||||||
**File:** `protocol/mizan-generate/generator/lib/adapters/svelte.mjs`
|
|
||||||
Memory leak if user forgets `onDestroy`.
|
|
||||||
|
|
||||||
### H12. Forms triggerValidation captures stale data
|
|
||||||
**File:** `mizan-react/src/forms.ts`
|
|
||||||
Debounced validation uses stale closure data.
|
|
||||||
|
|
||||||
## Remaining Medium
|
|
||||||
|
|
||||||
### M1. SSR bridge not fork-safe
|
|
||||||
gunicorn prefork shares file descriptors and Redis connections.
|
|
||||||
|
|
||||||
### M2. cache_purge_user() not implemented
|
|
||||||
No way to purge all cache entries for one user.
|
|
||||||
|
|
||||||
### M3. No garbage collection for context entries
|
|
||||||
Runtime `contexts` Map grows monotonically.
|
|
||||||
|
|
||||||
### M4. No cross-tab invalidation
|
|
||||||
No BroadcastChannel. Logout in tab 1 doesn't affect tab 2.
|
|
||||||
|
|
||||||
### M5. React 18 Strict Mode double-fetch
|
|
||||||
useEffect runs twice in dev mode.
|
|
||||||
|
|
||||||
### M6. No request deduplication
|
|
||||||
Two components mounting same context fire parallel fetches.
|
|
||||||
|
|
||||||
### M7. SSR worker module cache never invalidates
|
|
||||||
Dynamic imports cached forever.
|
|
||||||
|
|
||||||
### M8. Vue injection key not exported
|
|
||||||
Can't inject directly without generated composables.
|
|
||||||
|
|
||||||
### M9. Vue onMounted won't pre-fetch in Vue SSR
|
|
||||||
Needs `onServerPrefetch` for Nuxt.
|
|
||||||
|
|
||||||
### M10. Svelte should use setContext/getContext
|
|
||||||
Module-level stores don't scope to component tree.
|
|
||||||
|
|
||||||
### M12. render_strategy heuristic uses hardcoded param names
|
|
||||||
Misses `member_id`, `customer_id`, non-English names.
|
|
||||||
|
|
||||||
### M13. initSession called for token-auth requests
|
|
||||||
Wastes GET /session/ round-trip for JWT/MWT apps.
|
|
||||||
|
|
||||||
### M14. Vue watch imported but unused
|
|
||||||
Params not watched — reactive param changes don't trigger refetch.
|
|
||||||
|
|
||||||
### M15. Vue mutation composables misleading `use` prefix
|
|
||||||
`export const useXxx = callXxx` — not a real composable.
|
|
||||||
|
|
||||||
### M16. Svelte mutation imports bypass Stage 1 index
|
|
||||||
Should import from `'../index'` consistently.
|
|
||||||
|
|
||||||
### M17. Side effects in React state updater
|
|
||||||
Context listeners called inside `setContextStore()` updater.
|
|
||||||
|
|
||||||
## Architectural / Cleanup Debt
|
|
||||||
|
|
||||||
### A1. Legacy MizanProvider not yet removed
|
|
||||||
**File:** `mizan-react/src/context.tsx` (~750 lines)
|
|
||||||
Superseded by the kernel (`mizan-base`) + generated React adapter (`useSyncExternalStore`). Still exported as `MizanProvider`, `useMizan`, `useMizanContext`, etc. Must be deleted or replaced with thin shims that call `configure()` + delegate to the new generated hooks.
|
|
||||||
|
|
||||||
### A2. Allauth pending extraction
|
|
||||||
**File:** `legacy/allauth/` (44 files)
|
|
||||||
Sitting in `legacy/` since the cleanup pass. Should become its own `mizan-django-allauth` package consuming Mizan's public API. Unblocks v1 mizan-react publishing.
|
|
||||||
|
|
||||||
### A3. Forms codegen not adapted to kernel
|
|
||||||
**File:** `mizan-react/src/forms.ts` (~1163 lines)
|
|
||||||
Still uses `useMizan().call()` from the legacy MizanProvider. Needs rewrite to use `mizanCall` from the kernel. Currently the only consumer of MizanProvider — blocks A1.
|
|
||||||
|
|
||||||
### A4. Codegen for Vue/Svelte not validated end-to-end
|
|
||||||
The Stage 2 templates produce code that compiles, but no example app exercises Vue or Svelte rendering against a live backend. React is the only adapter with full integration verification.
|
|
||||||
|
|
||||||
### A5. ROADMAP.md is stale
|
|
||||||
**File:** `ROADMAP.md`
|
|
||||||
Lists SSR Bridge, Edge Manifest, Codegen Rewrite, etc. as "Next" — all are done. Doesn't reflect:
|
|
||||||
- Two-stage codegen with Vue/Svelte adapters
|
|
||||||
- C6 kernel-owned state (`ContextState<T>`)
|
|
||||||
- mizan-ts cross-language adapter
|
|
||||||
- Cleanup of djarea/Django-specific naming
|
|
||||||
|
|
||||||
### A6. CLAUDE.md may also be stale
|
|
||||||
**File:** `CLAUDE.md`
|
|
||||||
Written before the kernel rewrite. References to MizanProvider responsibilities and the old codegen pattern are likely outdated. Needs audit.
|
|
||||||
|
|
||||||
## Test Coverage Gaps
|
|
||||||
|
|
||||||
### T1. No tests for C6 kernel state machine
|
|
||||||
**File:** `mizan-base/` has no `tests/` directory at all
|
|
||||||
The state-owning kernel has zero unit tests. No coverage of:
|
|
||||||
- `registerContext` returning `getState/subscribe/refetch/unregister`
|
|
||||||
- Status transitions: idle → loading → success/error
|
|
||||||
- Subscriber notifications on state change
|
|
||||||
- Refetch reusing the same entry on Strict Mode re-mount
|
|
||||||
- `unregister` clearing listeners
|
|
||||||
|
|
||||||
### T2. No tests for generated Vue adapter output
|
|
||||||
The `vue.mjs` template produces code, but no test verifies it generates valid Vue 3 composables, that `onServerPrefetch` is wired correctly, or that the kernel subscription bridges to Vue reactivity.
|
|
||||||
|
|
||||||
### T3. No tests for generated Svelte adapter output
|
|
||||||
Same as T2. Readable store factory pattern is unverified against actual Svelte components.
|
|
||||||
|
|
||||||
### T4. No tests for view-path cache purge (C7 fix unverified)
|
|
||||||
The fix added `_purge_cache_for_invalidation()` to the view-path branch, but no test asserts that an `HttpResponse`-returning mutation actually purges the origin cache.
|
|
||||||
|
|
||||||
### T5. No tests for SSR thread safety (C4 fix unverified)
|
|
||||||
The `_write_lock` was added but no concurrent-render test exists to prove it prevents JSON interleaving.
|
|
||||||
|
|
||||||
### T6. No tests for SSR atexit cleanup (C5 fix unverified)
|
|
||||||
`atexit.register(self.shutdown)` was added but not exercised — no test that asserts the Bun process is reaped on Python exit.
|
|
||||||
|
|
||||||
### T7. No tests for SSR hydration injection (C3 fix unverified)
|
|
||||||
The `<script>window.__MIZAN_SSR_DATA__=...</script>` was added to template output but no test asserts it appears in rendered HTML or that the JSON is valid/safe.
|
|
||||||
|
|
||||||
### T8. No cross-language HMAC pin test for booleans/None (H11 fix unverified)
|
|
||||||
Python now normalizes True→"true", but there's no test comparing Python's `derive_cache_key(secret, ctx, {flag: True})` against TypeScript's equivalent to prove they produce identical hex output.
|
|
||||||
|
|
||||||
### T9. No tests for retry logic (H3)
|
|
||||||
`fetchWithRetry` retries 5xx/network errors with backoff. No test for: 5xx triggers retry, 4xx does not, mutation calls bypass retry, max retries respected.
|
|
||||||
|
|
||||||
### T10. No end-to-end integration test
|
|
||||||
Nothing exercises the full pipeline: Django function defined → schema exported → codegen runs → generated React mounts → mutation fires → server response includes invalidate → kernel refetches → DOM updates. Each layer is tested in isolation.
|
|
||||||
|
|
||||||
### T11. No tests for `isValid` requiring all required fields touched (H13 fix unverified)
|
|
||||||
The forms fix checks `field.required && !touched` but no test exercises a form with untouched required fields to confirm `isValid === false`.
|
|
||||||
|
|
||||||
### T12. No tests for `_meta` fresh-dict isolation (H10 fix unverified)
|
|
||||||
The shared-dict fix replaced `{**FunctionWrapper._meta, **meta}` with `{**meta}`. No test confirms that mutating one function's `_meta` doesn't leak into others.
|
|
||||||
|
|||||||
15
MIZAN.md
15
MIZAN.md
@@ -1,14 +1,19 @@
|
|||||||
# MIZAN — Named Contexts & Mutation Architecture
|
# MIZAN — Named Contexts & Mutation Architecture
|
||||||
|
|
||||||
|
> **Historical design spec.** The original named-contexts / mutation design
|
||||||
|
> document from the January 2025 design conversation. Kept as a record of design
|
||||||
|
> intent, not as a description of the current build — names and surfaces here
|
||||||
|
> predate the implementation (the codegen is the Rust binary
|
||||||
|
> `protocol/mizan-codegen`, never shipped under the working name "Maison"). For
|
||||||
|
> current architecture, read `CLAUDE.md` (wire protocol, package layout, codegen
|
||||||
|
> state) and `docs/` (`AFI_ARCHITECTURE.md`, `SSR_ARCHITECTURE.md`,
|
||||||
|
> `CACHE_KEYING.md`, `MWT_SPEC.md`).
|
||||||
|
|
||||||
## For Claude Code
|
## For Claude Code
|
||||||
|
|
||||||
This plan was written by Ryth's Claude.ai session after an extended design conversation
|
This plan was written by Ryth's Claude.ai session after an extended design conversation
|
||||||
reviewing the full codebase, the original @compose discussion from January 2025, and
|
reviewing the full codebase, the original @compose discussion from January 2025, and
|
||||||
several rounds of architectural refinement. Treat this as the spec.
|
several rounds of architectural refinement.
|
||||||
|
|
||||||
The framework formerly called mizan is now called **MIZAN**. Package names, imports,
|
|
||||||
and references should be updated accordingly. The internal codegen engine is called
|
|
||||||
**Maison** — it lives inside Mizan and does not need its own public surface.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
65
ROADMAP.md
65
ROADMAP.md
@@ -4,47 +4,38 @@
|
|||||||
|
|
||||||
### Done
|
### Done
|
||||||
|
|
||||||
- **`@client` decorator** — `context=`, `affects=`, `auth=`, `websocket=`, `private=`, `route=`, `methods=`, `rev=`, `cache=`
|
- [x] **`@client` decorator** — `context=`, `affects=`, `auth=`, `websocket=`, `private=`, `route=`, `methods=`, `rev=`, `cache=`
|
||||||
- **`ReactContext` class** — type-safe context/affects references with linting
|
- [x] **`ReactContext` class** — type-safe context/affects references with linting
|
||||||
- **Named contexts** — functions sharing a context name grouped into one provider and one fetch
|
- [x] **Named contexts** — functions sharing a context name grouped into one provider and one fetch
|
||||||
- **Context bundling endpoint** — `GET /api/mizan/ctx/<name>/` returns all functions in one response
|
- [x] **Context bundling endpoint** — `GET /api/mizan/ctx/<name>/` returns all functions in one response
|
||||||
- **Server-driven invalidation (JSON body)** — mutation responses carry `{"result": ..., "invalidate": [...]}`
|
- [x] **Server-driven invalidation (JSON body)** — mutation responses carry `{"result": ..., "invalidate": [...]}`
|
||||||
- **`X-Mizan-Invalidate` header** — second invalidation transport for view-path responses (redirects, HTML)
|
- [x] **`X-Mizan-Invalidate` header** — second invalidation transport for view-path responses (redirects, HTML)
|
||||||
- **Return-type branching** — data return → RPC path; `HttpResponse` return → view path
|
- [x] **Return-type branching** — data return → RPC path; `HttpResponse` return → view path
|
||||||
- **Scoped invalidation** — `affects_params` lambda; runtime supports `{context, params}` form
|
- [x] **Scoped invalidation** — `affects_params` lambda; runtime supports `{context, params}` form
|
||||||
- **Auth guards** — `auth=True`, `auth='staff'`, `auth='superuser'`, `auth=callable`
|
- [x] **Auth guards** — `auth=True`, `auth='staff'`, `auth='superuser'`, `auth=callable`
|
||||||
- **JWT + session auth** — auto-detected, CSRF handled
|
- [x] **JWT + session auth** — auto-detected, CSRF handled
|
||||||
- **MWT** — Mizan Web Token for Edge cache keying (separate secret from JWT/cache)
|
- [x] **MWT** — Mizan Web Token for Edge cache keying (separate secret from JWT/cache)
|
||||||
- **Shapes** — Pydantic + django-readers for typed query projections
|
- [x] **Shapes** — Pydantic + django-readers for typed query projections
|
||||||
- **WebSocket channels** — real-time bidirectional communication
|
- [x] **WebSocket channels** — real-time bidirectional communication
|
||||||
- **HMAC cache keying** — origin-side cache with cross-language HMAC conformance (Python + TypeScript pin)
|
- [x] **HMAC cache keying** — origin-side cache with cross-language HMAC conformance (Python + TypeScript pin)
|
||||||
- **Edge manifest** — `python manage.py export_edge_manifest`; both RPC and view-path functions
|
- [x] **Edge manifest** — `python manage.py export_edge_manifest`; both RPC and view-path functions; deterministic (sorted) output
|
||||||
- **SSR bridge** — Django template backend → persistent Bun subprocess via JSON-RPC
|
- [x] **SSR bridge** — Django template backend → persistent Bun subprocess via JSON-RPC; the worker resolves components by file path (`import(file)` + `renderToString`)
|
||||||
- **`mizan-base` kernel** — framework-agnostic imperative client primitives (data/status/error owned by kernel)
|
- [x] **`mizan-base` kernel** — framework-agnostic imperative client primitives (data/status/error owned by kernel)
|
||||||
- **Two-stage codegen** — Stage 1 emits framework-agnostic protocol layer; Stage 2 emits per-framework hooks (React, Vue, Svelte)
|
- [x] **Rust codegen** — `protocol/mizan-codegen`, a Rust binary reading KDL IR and emitting per-target clients (react, vue, svelte, channels, stage1, python, rust), each byte-parity-tested. `mizan-generate` is the thin npm launcher.
|
||||||
- **`mizan-ts`** — TypeScript backend adapter; proves the protocol is language-agnostic
|
- [x] **React wrapper layer** — codegen emits the `MizanContext` root provider, `useMizan` escape hatch, and `useMutation`-backed hooks exposing `{ mutate, isPending, error }`
|
||||||
|
- [x] **Additional backend adapters** — `mizan-ts` (TypeScript), `mizan-rust-axum` (Rust/Axum with three-way parity), `mizan-tauri`
|
||||||
|
- [x] **Frontend transports** — `mizan-tauri-transport`, `mizan-webview-transport`, `mizan-webview-channels`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Next (in progress)
|
### Next
|
||||||
|
|
||||||
- **React adapter wrapper layer** — codegen emits `MizanContext` provider, `useMizan` hook, `DjangoError` class on top of the `mizan-base` kernel. Equivalent wrapper layers for Vue and Svelte adapters. The harness in `examples/django-react-site` is blocked on this.
|
- [ ] **Vue / Svelte runtime packages** — `frontends/mizan-vue` and `frontends/mizan-svelte` are unimplemented stubs. The codegen emits their clients (byte-parity-tested), but a kernel-adapter runtime package and a live-backend example are owed for each.
|
||||||
- **Legacy `MizanProvider` removal (A1)** — `mizan-react/src/context.tsx` (~750 lines) replaced by codegen-emitted wrappers. Blocks v1 `mizan-react` publishing.
|
- [ ] **Svelte 5 runes** — the Svelte target emits Svelte 4 `readable` stores; migrate to `$state`/`$derived`.
|
||||||
- **Forms migration to kernel (A3)** — `mizan-react/src/forms.ts` (~1163 lines) currently consumes legacy `MizanProvider`. Rewrite to use `mizanCall` from the kernel. Blocks A1.
|
- [ ] **Forms codegen target** — emit form clients wired to `mizanCall` from the kernel; retire the hand-written `mizan-react/src/forms.ts` and its dependence on the pre-kernel provider.
|
||||||
- **Allauth extraction (A2)** — `legacy/allauth/` becomes `mizan-django-allauth` package consuming Mizan's public API.
|
- [ ] **Desktop example onto the generated provider** — migrate `examples/django-react-desktop-app` off the pre-kernel `MizanProvider` (`mizan-react/src/context.tsx`) so it can be retired.
|
||||||
- **Vue/Svelte e2e validation (A4)** — example apps exercising a live backend end-to-end, like `examples/django-react-site` does for React.
|
- [ ] **Cache hardening** — purge atomicity, per-param sub-index cleanup, thundering-herd protection, RedisCache coverage (see `backends/mizan-django/src/mizan/cache/KNOWN_ISSUES.md`).
|
||||||
- **Test coverage gaps** — T1–T12 in `ISSUES.md` (kernel state machine, view-path purge, SSR thread safety, retry logic, cross-language HMAC pin, etc.)
|
- [ ] **Package READMEs** — `mizan-base`, `mizan-codegen`, and the other packages missing one (see `ISSUES.md`).
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Quality
|
|
||||||
|
|
||||||
- **H5** — Mutation hooks expose no loading/error state
|
|
||||||
- **H7** — Redis SCAN blocks request path at scale
|
|
||||||
- **H8** — Svelte codegen uses Svelte 4 stores; should use Svelte 5 runes
|
|
||||||
- **H9** — Svelte `destroy()` not auto-called (memory leak)
|
|
||||||
- **H12** — Forms `triggerValidation` captures stale data
|
|
||||||
- Medium issues (M1–M18) per developer judgment
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -144,40 +144,33 @@ Frontend gets `useChatChannel({ room })`.
|
|||||||
|
|
||||||
## Generate the frontend
|
## Generate the frontend
|
||||||
|
|
||||||
The codegen is `mizan-generate` (in `protocol/mizan-generate/`). From your
|
The codegen is the `mizan-generate` Rust binary (source at
|
||||||
frontend project, point a config at the Django backend and run the CLI:
|
`protocol/mizan-codegen/`; `protocol/mizan-generate/` is a thin npm
|
||||||
|
launcher that dispatches to the platform binary). From your frontend
|
||||||
|
project, point a `mizan.toml` at the Django backend and run the CLI:
|
||||||
|
|
||||||
```js
|
```toml
|
||||||
// frontend/django.config.mjs
|
# frontend/mizan.toml
|
||||||
import path from "path"
|
output = "src/api"
|
||||||
import { fileURLToPath } from "url"
|
targets = ["react"]
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
[source.django]
|
||||||
const root = path.resolve(__dirname, "..")
|
manage_path = "../backend/manage.py"
|
||||||
|
command = ["uv", "run", "python"] # optional — defaults to ["python"]
|
||||||
|
|
||||||
export default {
|
[source.django.env]
|
||||||
source: {
|
PYTHONPATH = "../backend"
|
||||||
django: {
|
DJANGO_SETTINGS_MODULE = "myproject.settings"
|
||||||
managePath: path.join(root, "backend/manage.py"),
|
|
||||||
command: ["uv", "run", "python"],
|
|
||||||
env: {
|
|
||||||
PYTHONPATH: path.join(root, "backend"),
|
|
||||||
DJANGO_SETTINGS_MODULE: "myproject.settings",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
output: "src/api",
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx mizan-generate --config django.config.mjs
|
mizan-generate --config mizan.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
The codegen drives Django's management command (`export_mizan_schema`) under
|
The codegen drives Django's management command (`export_mizan_ir`) under
|
||||||
the hood, then emits Stage 1 (typed `callXxx`/`fetchXxx` over the runtime
|
the hood, parses the emitted KDL IR, then emits Stage 1 (typed
|
||||||
kernel) + Stage 2 (`<MizanContext>` provider, per-context providers,
|
`callXxx`/`fetchXxx` over the runtime kernel) + Stage 2 (`<MizanContext>`
|
||||||
`use{Hook}()` hooks) into `src/api/`.
|
provider, per-context providers, `use{Hook}()` hooks) into `src/api/`.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// app.tsx
|
// app.tsx
|
||||||
|
|||||||
@@ -1,65 +1,40 @@
|
|||||||
# Cache Module — Known Issues
|
# Cache Module — Known Issues
|
||||||
|
|
||||||
Issues identified by 8-domain-expert review. Status tracked here.
|
Open issues against the current cache implementation. Resolved items are
|
||||||
|
removed once their fix lands.
|
||||||
|
|
||||||
## Critical (Security / Data Corruption)
|
## Correctness
|
||||||
|
|
||||||
### 1. ~~User-scoped content cached without user_id~~ FIXED
|
### Purge race condition (non-atomic index operations)
|
||||||
`context_fetch_view` now extracts `user_id` from `request.user.pk` and
|
`cache_purge` reads the index and deletes as separate operations. A
|
||||||
passes it to `cache_get`/`cache_put`.
|
concurrent `cache_put` between the two steps can orphan entries. Mitigated
|
||||||
|
by AND-intersection purge semantics, but full atomicity (Lua script or
|
||||||
|
`WATCH`/`MULTI` on the Redis backend) is still owed.
|
||||||
|
|
||||||
### 2. Purge race condition (non-atomic index operations)
|
### Cross-language stringification divergence
|
||||||
`cache_purge` does index reads and deletes as separate operations.
|
Python `str(True)` → `"True"` vs JS `String(true)` → `"true"`. `_normalize`
|
||||||
Concurrent `cache_put` between steps can orphan entries.
|
canonicalizes `True`/`False`/`None` today, but the rules for the remaining
|
||||||
**Status:** Partially mitigated by AND semantics fix. Full atomicity
|
value types are not yet pinned in the protocol spec — so Python and
|
||||||
(Lua script or WATCH/MULTI) still needed for Redis backend.
|
TypeScript HMAC keys can still diverge on an un-normalized type.
|
||||||
|
|
||||||
### 3. ~~No Redis error handling~~ FIXED
|
## Performance / Operability
|
||||||
All cache operations in `executor.py` wrapped in try/except with
|
|
||||||
`logger.warning`. Redis failure falls through to uncached execution.
|
|
||||||
|
|
||||||
### 4. ~~Scoped purge uses OR semantics~~ FIXED
|
### Broad purge leaves per-param sub-indexes
|
||||||
Changed to AND (intersection). `{user_id: 5, org_id: 3}` now only
|
A broad `cache_purge(context)` deletes the entries but not the per-param
|
||||||
deletes entries matching BOTH params.
|
sub-indexes — a slow Redis memory leak.
|
||||||
|
|
||||||
## High (Correctness / Operability)
|
### No thundering-herd protection
|
||||||
|
Concurrent cold misses on the same key all execute and write. No
|
||||||
|
single-flight / request-coalescing.
|
||||||
|
|
||||||
### 5. ~~No TTL on Redis entries~~ FIXED
|
## API shape
|
||||||
`RedisCache.put` now sets `ex=86400` (24h safety-net TTL) by default.
|
|
||||||
|
|
||||||
### 6. Cross-language str() vs String() divergence
|
### cache_get / cache_put argument inconsistency
|
||||||
Python `str(True)` -> `"True"`, JS `String(true)` -> `"true"`.
|
`cache_get`/`cache_put` take explicit args while the executor resolves some
|
||||||
**Status:** Open. Needs canonical stringification rules in protocol spec.
|
inputs from module globals — two access patterns for one concern.
|
||||||
|
|
||||||
### 7. Broad purge doesn't clean per-param sub-indexes
|
## Coverage
|
||||||
**Status:** Open. Slow memory leak in Redis.
|
|
||||||
|
|
||||||
### 8. ~~build_index_keys doesn't stringify values~~ FIXED
|
### RedisCache lacks test coverage
|
||||||
Now calls `str(v)` on all values, matching `derive_cache_key`.
|
Only `MemoryCache` is exercised by the suite. `RedisCache` (connection
|
||||||
|
pooling, TTL, SCAN/UNLINK batching, socket timeouts) is untested.
|
||||||
### 9. ~~Silent exception swallowing in get_cache()~~ FIXED
|
|
||||||
Now logs warnings for partial config and connection failures.
|
|
||||||
|
|
||||||
### 10. ~~_initialized flag not thread-safe~~ FIXED
|
|
||||||
Now uses `threading.Lock` for thread-safe initialization.
|
|
||||||
|
|
||||||
## Medium (Design / Performance)
|
|
||||||
|
|
||||||
### 11. No thundering-herd protection
|
|
||||||
**Status:** Open. Concurrent cold misses all execute and write.
|
|
||||||
|
|
||||||
### 12. ~~Wire-protocol internals in __all__~~ FIXED
|
|
||||||
`derive_cache_key` and `build_index_keys` removed from `__all__`.
|
|
||||||
|
|
||||||
### 13. Inconsistent API pattern
|
|
||||||
**Status:** Open. `cache_get`/`cache_put` take explicit args but executor
|
|
||||||
fetches from globals.
|
|
||||||
|
|
||||||
### 14. ~~clear() uses SCAN + DELETE without pipeline~~ FIXED
|
|
||||||
Now uses pipeline with UNLINK for batched async deletes.
|
|
||||||
|
|
||||||
### 15. ~~No Redis connection timeouts~~ FIXED
|
|
||||||
`socket_connect_timeout=5`, `socket_timeout=5`, `health_check_interval=30`.
|
|
||||||
|
|
||||||
### 16. No RedisCache test coverage
|
|
||||||
**Status:** Open. Only MemoryCache is tested.
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ def generate_edge_manifest(
|
|||||||
|
|
||||||
manifest: dict[str, Any] = {"version": 1, "contexts": {}, "mutations": {}}
|
manifest: dict[str, Any] = {"version": 1, "contexts": {}, "mutations": {}}
|
||||||
|
|
||||||
for ctx_name, fn_names in groups.items():
|
for ctx_name, fn_names in sorted(groups.items()):
|
||||||
param_names: set[str] = set()
|
param_names: set[str] = set()
|
||||||
functions_meta: list[dict[str, Any]] = []
|
functions_meta: list[dict[str, Any]] = []
|
||||||
page_routes: list[str] = []
|
page_routes: list[str] = []
|
||||||
@@ -107,7 +107,7 @@ def generate_edge_manifest(
|
|||||||
|
|
||||||
manifest["contexts"][ctx_name] = ctx_entry
|
manifest["contexts"][ctx_name] = ctx_entry
|
||||||
|
|
||||||
for fn_name, fn_cls in all_functions.items():
|
for fn_name, fn_cls in sorted(all_functions.items()):
|
||||||
meta = getattr(fn_cls, "_meta", {})
|
meta = getattr(fn_cls, "_meta", {})
|
||||||
if not meta.get("affects"):
|
if not meta.get("affects"):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ from django.conf import settings as django_settings
|
|||||||
class mizanSettings:
|
class mizanSettings:
|
||||||
"""mizan configuration."""
|
"""mizan configuration."""
|
||||||
|
|
||||||
# Whether to expose function names in DEBUG mode errors
|
|
||||||
debug_expose_names: bool
|
|
||||||
|
|
||||||
# Cache HMAC signing secret (required when cache is enabled)
|
# Cache HMAC signing secret (required when cache is enabled)
|
||||||
cache_secret: str | None
|
cache_secret: str | None
|
||||||
|
|
||||||
@@ -36,12 +33,10 @@ def get_settings() -> mizanSettings:
|
|||||||
Load mizan settings from Django settings.
|
Load mizan settings from Django settings.
|
||||||
|
|
||||||
Settings:
|
Settings:
|
||||||
mizan_DEBUG_EXPOSE_NAMES: Show function names in errors when DEBUG=True (default: True)
|
|
||||||
MIZAN_CACHE_SECRET: HMAC signing key for cache keys (default: None)
|
MIZAN_CACHE_SECRET: HMAC signing key for cache keys (default: None)
|
||||||
MIZAN_CACHE_REDIS_URL: Redis connection URL (default: None)
|
MIZAN_CACHE_REDIS_URL: Redis connection URL (default: None)
|
||||||
"""
|
"""
|
||||||
return mizanSettings(
|
return mizanSettings(
|
||||||
debug_expose_names=getattr(django_settings, "mizan_DEBUG_EXPOSE_NAMES", True),
|
|
||||||
cache_secret=getattr(django_settings, "MIZAN_CACHE_SECRET", None),
|
cache_secret=getattr(django_settings, "MIZAN_CACHE_SECRET", None),
|
||||||
cache_redis_url=getattr(django_settings, "MIZAN_CACHE_REDIS_URL", None),
|
cache_redis_url=getattr(django_settings, "MIZAN_CACHE_REDIS_URL", None),
|
||||||
mwt_secret=getattr(django_settings, "MIZAN_MWT_SECRET", None),
|
mwt_secret=getattr(django_settings, "MIZAN_MWT_SECRET", None),
|
||||||
|
|||||||
@@ -108,37 +108,30 @@ anonymous request. The executor branches on those for `auth=True`,
|
|||||||
|
|
||||||
## Generate the frontend
|
## Generate the frontend
|
||||||
|
|
||||||
The codegen is `mizan-generate` (in `protocol/mizan-generate/`). Point a
|
The codegen is the `mizan-generate` Rust binary (source at
|
||||||
config at your FastAPI app and run the CLI:
|
`protocol/mizan-codegen/`; `protocol/mizan-generate/` is a thin npm
|
||||||
|
launcher that dispatches to the platform binary). Point a `mizan.toml` at
|
||||||
|
your FastAPI app and run the CLI:
|
||||||
|
|
||||||
```js
|
```toml
|
||||||
// frontend/fastapi.config.mjs
|
# frontend/mizan.toml
|
||||||
import path from "path"
|
output = "src/api"
|
||||||
import { fileURLToPath } from "url"
|
targets = ["react"]
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
[source.fastapi]
|
||||||
const root = path.resolve(__dirname, "..")
|
module = "main" # module to import for @client side effects
|
||||||
|
cwd = "../backend" # python cwd for module resolution
|
||||||
export default {
|
command = ["uv", "run", "python"] # optional — defaults to ["python"]
|
||||||
source: {
|
|
||||||
fastapi: {
|
|
||||||
module: "main", // module to import for @client side effects
|
|
||||||
cwd: path.join(root, "backend"), // python cwd for module resolution
|
|
||||||
command: ["uv", "run", "python"], // optional — defaults to ["python"]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
output: "src/api",
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx mizan-generate --config fastapi.config.mjs
|
mizan-generate --config mizan.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
The codegen drives `python -m mizan_fastapi.cli <module>` under the hood,
|
The codegen drives `python -m mizan_fastapi.ir <module>` under the hood,
|
||||||
then emits Stage 1 (typed `callXxx`/`fetchXxx` over the runtime kernel) +
|
parses the emitted KDL IR, then emits Stage 1 (typed `callXxx`/`fetchXxx`
|
||||||
Stage 2 (`<MizanContext>` provider, per-context providers, `use{Hook}()`
|
over the runtime kernel) + Stage 2 (`<MizanContext>` provider, per-context
|
||||||
hooks) into `src/api/`.
|
providers, `use{Hook}()` hooks) into `src/api/`.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// app.tsx
|
// app.tsx
|
||||||
@@ -171,13 +164,13 @@ uv run pytest
|
|||||||
For codegen consumption (or any tooling that wants the Mizan schema):
|
For codegen consumption (or any tooling that wants the Mizan schema):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m mizan_fastapi.cli <module>
|
python -m mizan_fastapi.ir <module>
|
||||||
```
|
```
|
||||||
|
|
||||||
Imports the named module (which must register every `@client` function as
|
Imports the named module (which must register every `@client` function as
|
||||||
import-time side effects), then prints the OpenAPI schema as JSON to stdout.
|
import-time side effects), then prints the Mizan KDL IR to stdout.
|
||||||
Mirrors mizan-django's `manage.py export_mizan_schema` so the codegen
|
Mirrors mizan-django's `manage.py export_mizan_ir` so the codegen consumes
|
||||||
consumes either backend the same subprocess way.
|
either backend the same subprocess way.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -52,10 +52,6 @@ function extractParams(fn: Function): ParamDef[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isResponseReturn(result: any): boolean {
|
|
||||||
return result instanceof Response
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function wrapper — registers a standalone function.
|
* Function wrapper — registers a standalone function.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ export interface MizanResponse {
|
|||||||
headers: Record<string, string>
|
headers: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortedStringify(data: any): string {
|
|
||||||
return JSON.stringify(data, Object.keys(data).sort())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle GET /api/mizan/ctx/:contextName/
|
* Handle GET /api/mizan/ctx/:contextName/
|
||||||
*
|
*
|
||||||
|
|||||||
54
cores/mizan-rust-macros/Cargo.lock
generated
Normal file
54
cores/mizan-rust-macros/Cargo.lock
generated
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mizan-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
@@ -11,16 +11,29 @@ Tree organized by role.
|
|||||||
backends/ server protocol adapters
|
backends/ server protocol adapters
|
||||||
mizan-django/ Django adapter
|
mizan-django/ Django adapter
|
||||||
mizan-fastapi/ FastAPI adapter (AFI-common scope)
|
mizan-fastapi/ FastAPI adapter (AFI-common scope)
|
||||||
|
mizan-rust-axum/ Rust/Axum adapter (handlers, errors, IR export)
|
||||||
|
mizan-tauri/ Tauri adapter — Mizan calls served in-process
|
||||||
mizan-ts/ TypeScript adapter (proves the protocol is language-agnostic)
|
mizan-ts/ TypeScript adapter (proves the protocol is language-agnostic)
|
||||||
frontends/ client kernel + per-framework adapters
|
frontends/ client kernel + per-framework adapters + transports
|
||||||
mizan-base/ framework-agnostic kernel; owns data, status, error; adapters subscribe
|
mizan-base/ framework-agnostic kernel (@mizan/base); owns data, status,
|
||||||
|
error; adapters subscribe through the MizanTransport interface
|
||||||
mizan-react/ React contexts + hooks over the kernel
|
mizan-react/ React contexts + hooks over the kernel
|
||||||
mizan-vue/ Vue composables over the kernel
|
mizan-vue/ Vue composables over the kernel
|
||||||
mizan-svelte/ Svelte stores/runes over the kernel
|
mizan-svelte/ Svelte stores/runes over the kernel
|
||||||
|
mizan-rust/ Rust client kernel
|
||||||
|
mizan-tauri-transport/ MizanTransport over Tauri IPC
|
||||||
|
mizan-webview-transport/ MizanTransport over a webview message channel
|
||||||
|
mizan-webview-channels/ channel transport over a webview bridge
|
||||||
cores/ shared language-level primitives
|
cores/ shared language-level primitives
|
||||||
mizan-python/ @client decorator, registry, MWT, HMAC cache keys
|
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
|
||||||
protocol/ protocol-level tooling
|
protocol/ protocol-level tooling
|
||||||
mizan-generate/ codegen — schema in, typed client out
|
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
|
workers/ runtime workers / bridges
|
||||||
mizan-ssr/ Bun subprocess used by the Django template backend
|
mizan-ssr/ Bun subprocess used by the Django template backend
|
||||||
```
|
```
|
||||||
@@ -35,11 +48,16 @@ compose.
|
|||||||
|
|
||||||
## Kernel model
|
## Kernel model
|
||||||
|
|
||||||
The client kernel (`mizan-base`) is the one hard thing. Per-
|
The client kernel (`@mizan/base`) is the one hard thing. It owns
|
||||||
framework adapters are thin idiomatic wrappers around it. Codegen
|
`ContextState<T> = {data, status, error}`, the context registry
|
||||||
emits typed bindings against the framework adapter's surface, not
|
(`registerContext`), `mizanCall` / `mizanFetch`, server-driven `merge`
|
||||||
against the raw kernel — so a React developer gets `useEcho()` and
|
and `invalidate`, and `initSession`. It reaches the backend through a
|
||||||
`<MizanContext>`, a Vue developer gets `useEcho()` composables, a
|
pluggable `MizanTransport` (`call` / `fetch`); the default is the
|
||||||
|
HTTP `httpTransport()`, swapped via `configure({ transport })` for
|
||||||
|
Tauri / webview hosts. Per-framework adapters are thin idiomatic
|
||||||
|
wrappers that subscribe to the kernel. Codegen emits typed bindings
|
||||||
|
against the framework adapter's surface — a React developer gets
|
||||||
|
`useEcho()` hooks, a Vue developer gets `useEcho()` composables, a
|
||||||
Svelte developer gets readable stores. Same kernel underneath.
|
Svelte developer gets readable stores. Same kernel underneath.
|
||||||
|
|
||||||
## KDL is the IR
|
## KDL is the IR
|
||||||
@@ -56,20 +74,23 @@ divergence between adapters is what the IR exists to prevent.
|
|||||||
|
|
||||||
Forward-direction primitives:
|
Forward-direction primitives:
|
||||||
|
|
||||||
- `cores/mizan-python` builds the IR from registered functions
|
- Each backend adapter emits KDL on stdout from an IR-export command:
|
||||||
(`build_ir()` walks `mizan_core.registry`, emits KDL)
|
FastAPI `python -m mizan_fastapi.ir <module>`, Django
|
||||||
- A `mizan-schema` package (forthcoming) holds the canonical KDL
|
`python manage.py export_mizan_ir`, Rust a consumer-side cargo bin
|
||||||
grammar / type system definition that every adapter targets
|
that calls `mizan_core::build_ir()`. Python's `build_ir()` walks
|
||||||
|
`mizan_core.registry`. The IR grammar (`type` / `function` /
|
||||||
|
`context` / `channel` nodes) is parsed by `mizan-codegen`'s
|
||||||
|
`src/ir.rs`; fixtures live at
|
||||||
|
`protocol/mizan-codegen/tests/fixtures/*.kdl`.
|
||||||
|
- `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
|
- Codegen reads KDL directly — no OpenAPI envelope, no
|
||||||
`openapi-typescript`, no per-backend converter divergence
|
`openapi-typescript`, no per-backend converter divergence. The
|
||||||
- Edge manifest, MWT claims, and other protocol artifacts all derive
|
former JavaScript/Node two-stage codegen (`openapi-typescript` plus
|
||||||
from the same KDL
|
`.mjs` adapters) has been deleted; codegen is now the single Rust
|
||||||
|
binary.
|
||||||
**Current implementation is transitional.** Today the codegen consumes
|
- Edge manifest, MWT claims, and other protocol artifacts derive from
|
||||||
OpenAPI 3.0 (`x-mizan-functions` + `x-mizan-contexts` extensions over
|
the same registry/IR.
|
||||||
Pydantic→JSON-Schema), produced via Django Ninja or FastAPI's native
|
|
||||||
generator. That layered indirection is what introduces adapter
|
|
||||||
divergence (see the AFI conformance suite). KDL-as-IR collapses it.
|
|
||||||
|
|
||||||
## Launch surface
|
## Launch surface
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,22 @@ standardized replacement exists.
|
|||||||
## Resolution: HMAC cache key (JSON-canonical form)
|
## Resolution: HMAC cache key (JSON-canonical form)
|
||||||
|
|
||||||
```
|
```
|
||||||
HMAC-SHA256(secret, JSON.stringify({
|
ctx:{context}:HMAC-SHA256(secret, json.dumps({
|
||||||
"c": context,
|
"c": context,
|
||||||
"p": sorted_params,
|
"p": sorted_params, // values normalized to JSON-native strings
|
||||||
"r": rev,
|
"r": rev,
|
||||||
"u": user_id // omitted for public content
|
"u": user_id // omitted for public content
|
||||||
}, sort_keys=True))
|
}, sort_keys=True, separators=(",", ":")))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`derive_cache_key(secret, context, params, user_id=None, rev=0)` →
|
||||||
|
`"ctx:{context}:{hmac_hex}"`. The `ctx:{context}:` prefix lets broad
|
||||||
|
purge SCAN by prefix. Param values are normalized for cross-language
|
||||||
|
consistency (`True`→`"true"`, `None`→`"null"`) before stringification.
|
||||||
|
Implemented in `cores/mizan-python/src/mizan_core/cache/keys.py` and
|
||||||
|
`backends/mizan-ts/src/cache/keys.ts` (`deriveCacheKey`); pin tests
|
||||||
|
verify identical output.
|
||||||
|
|
||||||
### Key derivation rules
|
### Key derivation rules
|
||||||
|
|
||||||
- **Public content** — URL path + query params (standard CDN).
|
- **Public content** — URL path + query params (standard CDN).
|
||||||
@@ -45,20 +53,24 @@ Mizan claims on `X-Mizan-Token` header. Replaces the old
|
|||||||
**Not a compiled binary ABI. Not a pluggable Python protocol.**
|
**Not a compiled binary ABI. Not a pluggable Python protocol.**
|
||||||
|
|
||||||
Each backend adapter (Python, TypeScript, future PHP/C#/Go)
|
Each backend adapter (Python, TypeScript, future PHP/C#/Go)
|
||||||
implements the cache protocol in its own language, backed by Redis.
|
implements the cache protocol in its own language.
|
||||||
**Conformance verified by a shared test suite.**
|
**Conformance verified by a shared test suite.**
|
||||||
|
|
||||||
### Required operations
|
### Required operations
|
||||||
|
|
||||||
- `cache_get`
|
- `cache_get`
|
||||||
- `cache_put`
|
- `cache_put`
|
||||||
- `cache_purge`
|
- `cache_purge` (scoped recomputes the key; broad SCANs the
|
||||||
- `cache_purge_user`
|
`ctx:{context}:*` prefix)
|
||||||
|
|
||||||
### Storage
|
### Storage
|
||||||
|
|
||||||
Redis only. Handles persistence, cross-worker sharing, crash
|
Two backends behind a `CacheBackend` protocol
|
||||||
recovery.
|
(`mizan_core/cache/backend.py`):
|
||||||
|
|
||||||
|
- `MemoryCache` — dict-based, for testing.
|
||||||
|
- `RedisCache` — production; persistence, cross-worker sharing, crash
|
||||||
|
recovery. Broad purge via SCAN, delete via UNLINK.
|
||||||
|
|
||||||
## Deploy invalidation
|
## Deploy invalidation
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ detection.
|
|||||||
| `pkey` | Deterministic hash of user's permission state at issuance |
|
| `pkey` | Deterministic hash of user's permission state at issuance |
|
||||||
| `exp` | Configurable short TTL — controls permission staleness window (Django setting) |
|
| `exp` | Configurable short TTL — controls permission staleness window (Django setting) |
|
||||||
| `iat` | Issued at |
|
| `iat` | Issued at |
|
||||||
| `kid` | Key ID — for secret rotation |
|
| `kid` | Key ID — for secret rotation. Carried in the JOSE header (RFC 7515), not the payload |
|
||||||
| `aud` | Audience binding — prevents cross-tenant replay |
|
| `aud` | Audience binding — prevents cross-tenant replay |
|
||||||
|
| `nbf` | Not-before — tolerates clock skew |
|
||||||
|
| `staff` / `super` | `is_staff` / `is_superuser`, used to build `MWTUser` without a DB query |
|
||||||
|
|
||||||
## Key decisions
|
## Key decisions
|
||||||
|
|
||||||
@@ -25,10 +27,14 @@ detection.
|
|||||||
- **`X-Mizan-Token` header, not `Authorization: Bearer`.** Avoids
|
- **`X-Mizan-Token` header, not `Authorization: Bearer`.** Avoids
|
||||||
collision with DRF, allauth, and existing JWT systems. Cloudflare
|
collision with DRF, allauth, and existing JWT systems. Cloudflare
|
||||||
WAF/Access do not inspect custom headers.
|
WAF/Access do not inspect custom headers.
|
||||||
- **Replaces `JWTUser` + `_try_jwt_auth` entirely.** Old approach is
|
- **`MWTUser`** is a minimal, DB-free request user built from the
|
||||||
deleted.
|
token claims (`cores/mizan-python/src/mizan_core/mwt.py`).
|
||||||
|
> A separate JWT module (`mizan/jwt/`) still exists for standard
|
||||||
|
> user-auth access/refresh tokens; MWT is the cache-keying identity
|
||||||
|
> layer, not a replacement for that module.
|
||||||
- **App handles authentication** (session, social, etc.). Mizan
|
- **App handles authentication** (session, social, etc.). Mizan
|
||||||
issues MWT *from* the authenticated identity.
|
issues MWT *from* the authenticated identity
|
||||||
|
(`create_mwt(user, secret, ttl, audience, kid)`).
|
||||||
- **Edge Worker** validates MWT, extracts `sub` for HMAC cache key,
|
- **Edge Worker** validates MWT, extracts `sub` for HMAC cache key,
|
||||||
checks `exp`.
|
checks `exp`.
|
||||||
- **`pkey` computation must be deterministic:**
|
- **`pkey` computation must be deterministic:**
|
||||||
@@ -43,9 +49,11 @@ detection.
|
|||||||
JSON with sorted keys:
|
JSON with sorted keys:
|
||||||
|
|
||||||
```
|
```
|
||||||
HMAC(secret, JSON.stringify({"c": context, "p": sorted_params, "u": user_id}))
|
ctx:{context}:HMAC(secret, JSON.stringify({"c": context, "p": sorted_params, "r": rev, "u": user_id}))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See [CACHE_KEYING.md](CACHE_KEYING.md) for the full derivation.
|
||||||
|
|
||||||
## What this solves
|
## What this solves
|
||||||
|
|
||||||
- DRF token collision
|
- DRF token collision
|
||||||
@@ -55,5 +63,8 @@ HMAC(secret, JSON.stringify({"c": context, "p": sorted_params, "u": user_id}))
|
|||||||
|
|
||||||
## Usage rule
|
## Usage rule
|
||||||
|
|
||||||
All cache-layer auth code uses MWT, not Django session or raw JWT.
|
MWT is the identity Edge/cache layers key on. The `@client(auth=...)`
|
||||||
The `@client(auth=...)` parameter gates on MWT validity.
|
parameter is enforced server-side in `mizan/client/executor.py`
|
||||||
|
(`_check_auth_requirement`), which checks `request.user` against the
|
||||||
|
auth requirement (`required` / `staff` / `superuser` / callable);
|
||||||
|
`request.user` may be an `MWTUser` (stateless) or a session user.
|
||||||
|
|||||||
@@ -22,17 +22,16 @@ multi-state privacy. ~$5–8K legal costs.
|
|||||||
TS "Deploy" exists via Workers for Platforms at no additional
|
TS "Deploy" exists via Workers for Platforms at no additional
|
||||||
compliance cost.
|
compliance cost.
|
||||||
|
|
||||||
## Free framework: mizan-cache (origin-side cache)
|
## Free framework: origin-side cache (`mizan.cache`)
|
||||||
|
|
||||||
Python package implementing the **full cache protocol locally** —
|
Shipped in `mizan_core.cache` (re-exported as `mizan.cache` from the
|
||||||
same HMAC key derivation, metadata schema, and purge semantics as
|
Django adapter) implementing the **full cache protocol locally** —
|
||||||
Edge.
|
same HMAC key derivation and purge semantics as Edge.
|
||||||
|
|
||||||
Three backends:
|
Two backends behind a `CacheBackend` protocol:
|
||||||
|
|
||||||
- In-memory dict (default)
|
- `MemoryCache` — in-memory dict (testing)
|
||||||
- Redis
|
- `RedisCache` — production
|
||||||
- SQLite
|
|
||||||
|
|
||||||
### Dual purpose
|
### Dual purpose
|
||||||
|
|
||||||
@@ -44,8 +43,8 @@ Three backends:
|
|||||||
## Spec additions
|
## Spec additions
|
||||||
|
|
||||||
- `@client(cache=False)` — uncacheable; emits `Cache-Control: no-store`.
|
- `@client(cache=False)` — uncacheable; emits `Cache-Control: no-store`.
|
||||||
- Cache ABI: `get(key)`, `put(key, response, metadata)`,
|
- Cache ABI (`mizan.cache`): `cache_get(secret, backend, context, params)`,
|
||||||
`purge(context, params)`.
|
`cache_put(...)`, `cache_purge(backend, context, params=…, secret=…)`.
|
||||||
|
|
||||||
## Launch compliance (Render only)
|
## Launch compliance (Render only)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ Works on a $5 VPS with local Bun. **No Edge required.** PSR is part
|
|||||||
of the protocol; it's available to every Mizan deployment regardless
|
of the protocol; it's available to every Mizan deployment regardless
|
||||||
of hosting.
|
of hosting.
|
||||||
|
|
||||||
|
> Current state: the Edge manifest records each context's
|
||||||
|
> `render_strategy` (`"psr"` for public, `"dynamic_cached"` for
|
||||||
|
> user-scoped) — see `mizan/export/` and the `export_edge_manifest`
|
||||||
|
> management command — and the SSR bridge can render a component to
|
||||||
|
> HTML. The render-on-mutation orchestration that wires those together
|
||||||
|
> (mutation → trigger local render → store HTML) is not yet present in
|
||||||
|
> the open-source backends; it is the manifest-driven behavior the
|
||||||
|
> Edge layer consumes.
|
||||||
|
|
||||||
## Edge Delivery — Mizan Render (Paid Product)
|
## Edge Delivery — Mizan Render (Paid Product)
|
||||||
|
|
||||||
Pre-rendered HTML cached globally on Cloudflare CDN.
|
Pre-rendered HTML cached globally on Cloudflare CDN.
|
||||||
|
|||||||
@@ -10,23 +10,31 @@ rendering engine.
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'mizan.ssr.MizanTemplates',
|
'BACKEND': 'mizan.ssr.MizanTemplates',
|
||||||
...
|
'DIRS': [BASE_DIR / 'frontend'],
|
||||||
|
'OPTIONS': {
|
||||||
|
'worker': 'path/to/mizan-ssr/src/worker.tsx',
|
||||||
|
'timeout': 5,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
Then `render(request, 'ProfilePage', context)` calls the Bun
|
Then `render(request, 'components/Hello.tsx', context)` calls the Bun
|
||||||
subprocess bridge instead of rendering a Django/Jinja2 template.
|
subprocess bridge instead of rendering a Django/Jinja2 template.
|
||||||
**The component name IS the template name.**
|
**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.
|
||||||
|
|
||||||
## AFI boundary
|
## AFI boundary
|
||||||
|
|
||||||
| Side | Responsibility |
|
| Side | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Backend adapter | Implements `mizan.ssr()` — executes context functions, gathers data |
|
| Backend adapter (`SSRBridge`) | Manages the Bun subprocess lifecycle; gathers props |
|
||||||
| Frontend adapter | Implements `renderToHTML()` — takes component + props, produces HTML |
|
| Bun worker (`worker.tsx`) | `import()`s the file path, `renderToString(createElement(Component, props))` |
|
||||||
| Bun subprocess | Hosts the frontend adapter |
|
| stdin/stdout JSON-RPC | Newline-delimited; `{id, method:"render", params:{file, props}}` → `{id, html}` / `{id, error}`; `ping` → `{id, pong:true}` |
|
||||||
| stdin/stdout JSON-RPC | Transport between the two |
|
|
||||||
|
|
||||||
## Why template backend
|
## Why template backend
|
||||||
|
|
||||||
@@ -35,16 +43,22 @@ subprocess bridge instead of rendering a Django/Jinja2 template.
|
|||||||
- Django developers already use `render(request, template, context)`
|
- Django developers already use `render(request, template, context)`
|
||||||
— no new API to learn.
|
— no new API to learn.
|
||||||
- URL routing, views, middleware, auth — all unchanged.
|
- URL routing, views, middleware, auth — all unchanged.
|
||||||
- The template tag `{% mizan_render %}` is a convenience for
|
|
||||||
developers who *also* use Django templates (e.g., a base.html shell
|
> A `templatetags/` package exists for a future `{% mizan_render %}`
|
||||||
with Mizan components inside).
|
> convenience tag (base.html shell with Mizan components inside), but
|
||||||
|
> it is currently empty — no tag is implemented yet.
|
||||||
|
|
||||||
## Implementation surface
|
## Implementation surface
|
||||||
|
|
||||||
The SSR bridge module implements Django's template backend interface:
|
The SSR backend (`mizan/ssr/backend.py`) implements Django's template
|
||||||
|
backend interface:
|
||||||
|
|
||||||
- `BaseEngine` subclass
|
- `MizanTemplates(BaseEngine)` — requires `OPTIONS['worker']` (path to
|
||||||
- `Template` class with `.render(context, request)`
|
`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
|
Everything Django expects from a template backend, but the actual
|
||||||
rendering routes to Bun.
|
rendering routes to Bun.
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'mizan/channels': path.join(reactPkg, 'channels/index.ts'),
|
'mizan/channels': path.join(reactPkg, 'channels/index.ts'),
|
||||||
'mizan/client/react': path.join(reactPkg, 'client/react.ts'),
|
'mizan/client/react': path.join(reactPkg, 'client/react.ts'),
|
||||||
'mizan/client/nextjs': path.join(reactPkg, 'client/nextjs.tsx'),
|
|
||||||
'mizan/client': path.join(reactPkg, 'client/index.ts'),
|
'mizan/client': path.join(reactPkg, 'client/index.ts'),
|
||||||
'mizan/jwt': path.join(reactPkg, 'jwt/index.ts'),
|
'mizan/jwt': path.join(reactPkg, 'jwt/index.ts'),
|
||||||
'mizan/allauth/nextjs': path.join(reactPkg, 'allauth/nextjs.tsx'),
|
|
||||||
'mizan/allauth': path.join(reactPkg, 'allauth/index.ts'),
|
|
||||||
'mizan': path.join(reactPkg, 'index.ts'),
|
'mizan': path.join(reactPkg, 'index.ts'),
|
||||||
'@rythazhur/mizan/channels': path.join(reactPkg, 'channels/index.ts'),
|
'@rythazhur/mizan/channels': path.join(reactPkg, 'channels/index.ts'),
|
||||||
'@rythazhur/mizan/jwt': path.join(reactPkg, 'jwt/index.ts'),
|
'@rythazhur/mizan/jwt': path.join(reactPkg, 'jwt/index.ts'),
|
||||||
|
|||||||
@@ -12,39 +12,44 @@ npm install @rythazhur/mizan@git+https://git.impactsoundworks.com/isw/mizan.git#
|
|||||||
|
|
||||||
You don't use this package directly. You use the **generated hooks**.
|
You don't use this package directly. You use the **generated hooks**.
|
||||||
|
|
||||||
|
This is the pre-kernel React adapter: it ships its own `MizanProvider`
|
||||||
|
(`src/context.tsx`) that owns HTTP/WebSocket/CSRF/session/context state
|
||||||
|
directly, rather than subscribing to the `@mizan/base` kernel. It is still
|
||||||
|
the provider the Django + desktop example wires against. (`DjangoContext`,
|
||||||
|
`useDjango`, etc. are deprecated aliases for the `Mizan*` names.)
|
||||||
|
|
||||||
### 1. Configure
|
### 1. Configure
|
||||||
|
|
||||||
```js
|
```toml
|
||||||
// django.config.mjs
|
# mizan.toml
|
||||||
export default {
|
output = "src/api"
|
||||||
source: {
|
targets = ["react"]
|
||||||
django: {
|
|
||||||
managePath: '../backend/manage.py',
|
[source.django]
|
||||||
command: ['uv', 'run', 'python'],
|
manage_path = "../backend/manage.py"
|
||||||
},
|
command = ["uv", "run", "python"]
|
||||||
},
|
|
||||||
output: 'src/api/generated.ts',
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Generate
|
### 2. Generate
|
||||||
|
|
||||||
|
The codegen is the `mizan-generate` Rust binary (source at
|
||||||
|
`protocol/mizan-codegen/`; `protocol/mizan-generate/` is the npm launcher):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx mizan-generate # once
|
mizan-generate --config mizan.toml
|
||||||
npx mizan-generate --watch # dev mode
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Wrap your app
|
### 3. Wrap your app
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { DjangoContext } from '@/api'
|
import { MizanProvider } from '@rythazhur/mizan'
|
||||||
|
|
||||||
<DjangoContext>
|
<MizanProvider>
|
||||||
<App />
|
<App />
|
||||||
</DjangoContext>
|
</MizanProvider>
|
||||||
```
|
```
|
||||||
|
|
||||||
`DjangoContext` is the only provider you need. It handles HTTP, WebSocket, CSRF, session init, context auto-fetching, and channel connections.
|
`MizanProvider` is the only provider you need. It handles HTTP, WebSocket, CSRF, session init, context auto-fetching, and channel connections.
|
||||||
|
|
||||||
### 4. Use generated hooks
|
### 4. Use generated hooks
|
||||||
|
|
||||||
@@ -71,19 +76,22 @@ chat.messages // typed, reactive
|
|||||||
|
|
||||||
## Generated Files
|
## Generated Files
|
||||||
|
|
||||||
|
The Rust codegen emits per-target files into the configured `output`
|
||||||
|
directory (Stage 1 is auto-included whenever `react` is a target):
|
||||||
|
|
||||||
| File | Contents |
|
| File | Contents |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `generated.django.tsx` | `DjangoContext` + typed hooks |
|
| `types.ts` | Pydantic types |
|
||||||
| `generated.mizan.ts` | Pydantic types |
|
| `contexts/<name>.ts` | Per-context `fetchXxx` bundles |
|
||||||
| `generated.forms.ts` | Form hooks with Zod |
|
| `react.tsx` | `<MizanContext>` provider + typed `use{Hook}()` hooks |
|
||||||
| `generated.channels.hooks.tsx` | Channel hooks |
|
| `channels.ts` / `channels.hooks.tsx` | Channel types + hooks (when the schema carries channels) |
|
||||||
| `index.ts` | Re-exports everything |
|
| `index.ts` | Stage 1 re-export root |
|
||||||
|
|
||||||
## Sub-exports
|
## Sub-exports
|
||||||
|
|
||||||
| Import | When to use |
|
| Import | When to use |
|
||||||
|--------|------------|
|
|--------|------------|
|
||||||
| `@rythazhur/mizan` | Core: mizanProvider, hooks, forms, errors |
|
| `@rythazhur/mizan` | Core: `MizanProvider`, hooks, forms, errors |
|
||||||
| `@rythazhur/mizan/channels` | WebSocket channels |
|
| `@rythazhur/mizan/channels` | WebSocket channels |
|
||||||
| `@rythazhur/mizan/jwt` | JWT token management |
|
| `@rythazhur/mizan/jwt` | JWT token management |
|
||||||
| `@rythazhur/mizan/client` | HTTP clients (CSR/SSR) |
|
| `@rythazhur/mizan/client` | HTTP clients (CSR/SSR) |
|
||||||
|
|||||||
@@ -17,10 +17,6 @@
|
|||||||
"types": "./dist/client/react.d.ts",
|
"types": "./dist/client/react.d.ts",
|
||||||
"import": "./dist/client/react.js"
|
"import": "./dist/client/react.js"
|
||||||
},
|
},
|
||||||
"./client/nextjs": {
|
|
||||||
"types": "./dist/client/nextjs.d.ts",
|
|
||||||
"import": "./dist/client/nextjs.js"
|
|
||||||
},
|
|
||||||
"./channels": {
|
"./channels": {
|
||||||
"types": "./dist/channels/index.d.ts",
|
"types": "./dist/channels/index.d.ts",
|
||||||
"import": "./dist/channels/index.js"
|
"import": "./dist/channels/index.js"
|
||||||
@@ -28,14 +24,6 @@
|
|||||||
"./jwt": {
|
"./jwt": {
|
||||||
"types": "./dist/jwt/index.d.ts",
|
"types": "./dist/jwt/index.d.ts",
|
||||||
"import": "./dist/jwt/index.js"
|
"import": "./dist/jwt/index.js"
|
||||||
},
|
|
||||||
"./allauth": {
|
|
||||||
"types": "./dist/allauth/index.d.ts",
|
|
||||||
"import": "./dist/allauth/index.js"
|
|
||||||
},
|
|
||||||
"./allauth/nextjs": {
|
|
||||||
"types": "./dist/allauth/nextjs.d.ts",
|
|
||||||
"import": "./dist/allauth/nextjs.js"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use indexmap::IndexMap;
|
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::emit::CodegenTarget;
|
use crate::emit::CodegenTarget;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
{% if has_contexts -%}
|
{% if has_contexts -%}
|
||||||
registerContext,
|
registerContext,
|
||||||
type ContextState,
|
type ContextState,
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
} from '@mizan/base'
|
} from '@mizan/base'
|
||||||
|
|
||||||
{% if !stage1_imports.is_empty() -%}
|
{% if !stage1_imports.is_empty() -%}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ fn fixture_config() -> Config {
|
|||||||
project_id: None,
|
project_id: None,
|
||||||
output: PathBuf::from("/tmp"),
|
output: PathBuf::from("/tmp"),
|
||||||
targets: vec!["channels".to_string()],
|
targets: vec!["channels".to_string()],
|
||||||
source: SourceConfig { fastapi: None, django: None },
|
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
|
||||||
rust_kernel: None,
|
rust_kernel: None,
|
||||||
rust_crate_name: None,
|
rust_crate_name: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
configure,
|
configure,
|
||||||
initSession,
|
|
||||||
mizanCall,
|
mizanCall,
|
||||||
mizanFetch,
|
mizanFetch,
|
||||||
MizanError,
|
|
||||||
registerContext,
|
registerContext,
|
||||||
type ContextState,
|
type ContextState,
|
||||||
} from '@mizan/base'
|
} from '@mizan/base'
|
||||||
@@ -45,6 +43,7 @@ function useContextSubscription<T>(
|
|||||||
return useSyncExternalStore(handle.subscribe, handle.getState, handle.getState)
|
return useSyncExternalStore(handle.subscribe, handle.getState, handle.getState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Internal — wraps an imperative call() with isPending / error state.
|
// Internal — wraps an imperative call() with isPending / error state.
|
||||||
interface MutationHook<TArgs, TResult> {
|
interface MutationHook<TArgs, TResult> {
|
||||||
mutate: (args: TArgs) => Promise<TResult>
|
mutate: (args: TArgs) => Promise<TResult>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ fn fixture_config() -> Config {
|
|||||||
project_id: None,
|
project_id: None,
|
||||||
output: PathBuf::from("/tmp"),
|
output: PathBuf::from("/tmp"),
|
||||||
targets: vec!["python".to_string()],
|
targets: vec!["python".to_string()],
|
||||||
source: SourceConfig { fastapi: None, django: None },
|
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
|
||||||
rust_kernel: None,
|
rust_kernel: None,
|
||||||
rust_crate_name: None,
|
rust_crate_name: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ fn fixture_config() -> Config {
|
|||||||
project_id: None,
|
project_id: None,
|
||||||
output: PathBuf::from("/tmp"),
|
output: PathBuf::from("/tmp"),
|
||||||
targets: vec!["react".to_string()],
|
targets: vec!["react".to_string()],
|
||||||
source: SourceConfig { fastapi: None, django: None },
|
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
|
||||||
rust_kernel: None,
|
rust_kernel: None,
|
||||||
rust_crate_name: None,
|
rust_crate_name: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ fn fixture_config() -> Config {
|
|||||||
project_id: None,
|
project_id: None,
|
||||||
output: PathBuf::from("/tmp"),
|
output: PathBuf::from("/tmp"),
|
||||||
targets: vec!["rust".to_string()],
|
targets: vec!["rust".to_string()],
|
||||||
source: SourceConfig { fastapi: None, django: None },
|
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
|
||||||
rust_kernel: Some(RustKernelSpec::Path {
|
rust_kernel: Some(RustKernelSpec::Path {
|
||||||
path: "../../../frontends/mizan-rust".to_string(),
|
path: "../../../frontends/mizan-rust".to_string(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ fn synthetic_config() -> Config {
|
|||||||
project_id: None,
|
project_id: None,
|
||||||
output: PathBuf::from("/tmp"),
|
output: PathBuf::from("/tmp"),
|
||||||
targets: vec!["stage1".to_string()],
|
targets: vec!["stage1".to_string()],
|
||||||
source: SourceConfig { fastapi: None, django: None },
|
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
|
||||||
rust_kernel: None,
|
rust_kernel: None,
|
||||||
rust_crate_name: None,
|
rust_crate_name: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ fn fixture_config(target: &str) -> Config {
|
|||||||
project_id: None,
|
project_id: None,
|
||||||
output: PathBuf::from("/tmp"),
|
output: PathBuf::from("/tmp"),
|
||||||
targets: vec![target.to_string()],
|
targets: vec![target.to_string()],
|
||||||
source: SourceConfig { fastapi: None, django: None },
|
source: SourceConfig { fastapi: None, django: None, rust: None, script: None },
|
||||||
rust_kernel: None,
|
rust_kernel: None,
|
||||||
rust_crate_name: None,
|
rust_crate_name: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test SSR worker — registers simple components for the test suite.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { registerComponent } from './worker'
|
|
||||||
|
|
||||||
// Simple component that renders props
|
|
||||||
function Hello({ name }: { name: string }) {
|
|
||||||
return <div>Hello, {name}!</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component that renders a list
|
|
||||||
function UserProfile({ user_id, name }: { user_id: number; name: string }) {
|
|
||||||
return (
|
|
||||||
<div className="profile">
|
|
||||||
<h1>{name}</h1>
|
|
||||||
<span>ID: {user_id}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component that throws during render
|
|
||||||
function Broken() {
|
|
||||||
throw new Error('Intentional render error')
|
|
||||||
}
|
|
||||||
|
|
||||||
registerComponent('Hello', Hello)
|
|
||||||
registerComponent('UserProfile', UserProfile)
|
|
||||||
registerComponent('Broken', Broken)
|
|
||||||
Reference in New Issue
Block a user