Cleanup: delete dead code, fix invalidateFunctions bug, deduplicate

Deleted:
- runtime/index.ts (146 lines) — never imported by anything
- httpFunctionCall + _csrClient cache — redundant third HTTP path
- 3 duplicate getCSRFToken() implementations → shared utils.ts

Fixed:
- invalidateFunctions() was ignoring function names and invalidating
  ALL mounted contexts. Now correctly passes names through.

33 React tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 03:36:33 -04:00
parent 1b5dca5ab3
commit 24ff0ae66d
6 changed files with 17 additions and 232 deletions

View File

@@ -148,19 +148,7 @@ export class HttpError extends Error {
// Internal Utilities
// =============================================================================
function getCookie(name: string): string | null {
if (typeof document === 'undefined') return null
const value = `; ${document.cookie}`
const parts = value.split(`; ${name}=`)
if (parts.length === 2) {
return parts.pop()?.split(';').shift() ?? null
}
return null
}
function getCSRFToken(): string | null {
return getCookie('csrftoken')
}
import { getCSRFToken } from '../utils'
interface RequestBuild {
request: RequestInit
@@ -537,49 +525,3 @@ export interface FunctionSuccessResponse<T> {
*/
export type FunctionResponse<T> = FunctionSuccessResponse<T> | FunctionErrorResponse
// Cached CSR client for server function calls
let _csrClient: DjangoHTTPClient | null = null
function getCSRClient(): DjangoHTTPClient {
if (!_csrClient) {
_csrClient = createDjangoCSRClient(Auth.SESSION)
}
return _csrClient
}
/**
* Call a Django server function via HTTP.
* Used as fallback when WebSocket is unavailable.
*
* Uses the standard CSR client with session-based auth.
*
* @param baseUrl - Base URL for the API (e.g., '/api/mizan')
* @param functionName - Name of the server function
* @param input - Input data for the function
* @returns Promise resolving to the function output
* @throws FunctionErrorResponse on failure
*/
export async function httpFunctionCall<TInput = unknown, TOutput = unknown>(
baseUrl: string,
functionName: string,
input?: TInput
): Promise<TOutput> {
const client = getCSRClient()
// Use request() not json() because server functions return { error: true/false }
// in the body, not HTTP status codes for business errors
const response = await client.request(
'POST',
`${baseUrl}/call/`,
{ fn: functionName, args: input }
)
const data: FunctionResponse<TOutput> = await response.json()
if (data.error) {
throw new DjangoError(data)
}
return (data as FunctionSuccessResponse<TOutput>).result
}

View File

@@ -40,15 +40,7 @@ import {
import { useJWT } from './jwt'
import { DjangoError, type ErrorCode, type FunctionErrorResponse } from './errors'
// ============================================================================
// Utilities
// ============================================================================
function getCSRFToken(): string | null {
if (typeof document === 'undefined') return null
const match = document.cookie.match(/csrftoken=([^;]+)/)
return match?.[1] ?? null
}
import { getCSRFToken } from './utils'
// ============================================================================
// Types
@@ -546,18 +538,10 @@ export function MizanProvider({
const invalidateFunctions = useCallback(
async (names: string[]): Promise<void> => {
// Each function belongs to a context. Invalidating a function
// means refetching its entire context (since the bundling endpoint
// returns all functions). Dedupe by context name.
const contexts = new Set<string>()
for (const name of names) {
// The context name for each function is known at codegen time
// and baked into the generated hook. Here we just invalidate
// whatever contexts are registered that contain these functions.
for (const [ctxName] of contextProvidersRef.current) {
contexts.add(ctxName)
}
}
// Function names are passed directly as context invalidation targets.
// The server already resolved function → context mapping.
// Dedupe and invalidate each.
const contexts = new Set(names)
await Promise.all(
Array.from(contexts).map(ctx => invalidateContext(ctx))
)

View File

@@ -62,7 +62,6 @@ export {
// ============================================================================
export {
httpFunctionCall,
createDjangoCSRClient,
createDjangoSSRClient,
ensureDjangoSession,

View File

@@ -10,12 +10,7 @@ import {
type ReactNode,
} from 'react'
import type { JWTTokens, JWTConfig, JWTState } from '../client/types'
function getCSRFToken(): string | null {
if (typeof document === 'undefined') return null
const match = document.cookie.match(/csrftoken=([^;]+)/)
return match?.[1] ?? null
}
import { getCSRFToken } from '../utils'
const Context = createContext<JWTState | null>(null)

View File

@@ -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({ fn: 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
}

View File

@@ -0,0 +1,10 @@
/**
* Shared utilities used across mizan-react.
*/
/** Extract CSRF token from cookies. Returns null during SSR. */
export function getCSRFToken(): string | null {
if (typeof document === 'undefined') return null
const match = document.cookie.match(/csrftoken=([^;]+)/)
return match?.[1] ?? null
}