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:
@@ -1,145 +0,0 @@
|
||||
/**
|
||||
* Mizan Runtime — The client state engine.
|
||||
*
|
||||
* Framework-agnostic. React, Vue, Svelte, Solid — all wrap this.
|
||||
*
|
||||
* Three concerns:
|
||||
* 1. Context registry — mounted providers register here for invalidation
|
||||
* 2. Invalidation — batched via microtask, supports scoped params
|
||||
* 3. Fetch — mizanFetch (GET context bundles) + mizanCall (POST mutations)
|
||||
*/
|
||||
|
||||
// === Types ===
|
||||
|
||||
export class MizanError extends Error {
|
||||
constructor(public status: number, public body: string) {
|
||||
super(`Mizan call failed (${status})`)
|
||||
}
|
||||
}
|
||||
|
||||
export type RefetchFn = () => void
|
||||
type ParamKey = string // JSON.stringify of params
|
||||
|
||||
interface ContextEntry {
|
||||
params: Record<string, any>
|
||||
refetch: RefetchFn
|
||||
}
|
||||
|
||||
// === Configuration ===
|
||||
|
||||
let config = {
|
||||
baseUrl: '/api/mizan',
|
||||
getHeaders: (): Record<string, string> => ({}),
|
||||
}
|
||||
|
||||
export function configure(opts: Partial<typeof config>) {
|
||||
Object.assign(config, opts)
|
||||
}
|
||||
|
||||
// === Context Registry ===
|
||||
// Mounted context providers register here. Unmounted ones deregister.
|
||||
|
||||
const contexts: Map<string, Map<ParamKey, ContextEntry>> = new Map()
|
||||
|
||||
export function registerContext(
|
||||
name: string,
|
||||
params: Record<string, any>,
|
||||
refetch: RefetchFn,
|
||||
): () => void {
|
||||
if (!contexts.has(name)) contexts.set(name, new Map())
|
||||
const key = JSON.stringify(params)
|
||||
contexts.get(name)!.set(key, { params, refetch })
|
||||
return () => contexts.get(name)!.delete(key)
|
||||
}
|
||||
|
||||
// === Invalidation ===
|
||||
// Batched via microtask. Multiple invalidations in the same tick coalesce.
|
||||
|
||||
const pending: Set<string> = new Set()
|
||||
const pendingScoped: Map<string, Record<string, any>> = new Map()
|
||||
let scheduled = false
|
||||
|
||||
export function invalidate(context: string, params?: Record<string, any>) {
|
||||
if (params) {
|
||||
pendingScoped.set(context, params)
|
||||
} else {
|
||||
pending.add(context)
|
||||
}
|
||||
if (!scheduled) {
|
||||
scheduled = true
|
||||
queueMicrotask(flush)
|
||||
}
|
||||
}
|
||||
|
||||
function flush() {
|
||||
// Broad invalidations — refetch all instances of context
|
||||
for (const name of pending) {
|
||||
const entries = contexts.get(name)
|
||||
if (entries) entries.forEach(entry => entry.refetch())
|
||||
}
|
||||
|
||||
// Scoped invalidations — refetch only matching params
|
||||
for (const [name, params] of pendingScoped) {
|
||||
if (pending.has(name)) continue // already refetched all
|
||||
const entries = contexts.get(name)
|
||||
if (!entries) continue
|
||||
const key = JSON.stringify(params)
|
||||
const entry = entries.get(key)
|
||||
if (entry) entry.refetch()
|
||||
}
|
||||
|
||||
pending.clear()
|
||||
pendingScoped.clear()
|
||||
scheduled = false
|
||||
}
|
||||
|
||||
// === Fetch ===
|
||||
|
||||
export async function mizanFetch(
|
||||
contextName: string,
|
||||
params?: Record<string, any>,
|
||||
): Promise<any> {
|
||||
const url = new URL(`${config.baseUrl}/ctx/${contextName}/`, globalThis.location?.origin ?? 'http://localhost')
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
url.searchParams.set(k, String(v))
|
||||
}
|
||||
}
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: { ...config.getHeaders(), 'Accept': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
if (!res.ok) throw new MizanError(res.status, await res.text())
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function mizanCall(
|
||||
functionName: string,
|
||||
args: Record<string, any>,
|
||||
): Promise<any> {
|
||||
const res = await fetch(`${config.baseUrl}/call/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...config.getHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ function: functionName, args }),
|
||||
})
|
||||
if (!res.ok) throw new MizanError(res.status, await res.text())
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
// Server-driven invalidation
|
||||
if (data.invalidate) {
|
||||
for (const entry of data.invalidate) {
|
||||
if (typeof entry === 'string') {
|
||||
invalidate(entry)
|
||||
} else {
|
||||
invalidate(entry.context, entry.params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data.result
|
||||
}
|
||||
@@ -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