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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
|
||||
@@ -62,7 +62,6 @@ export {
|
||||
// ============================================================================
|
||||
|
||||
export {
|
||||
httpFunctionCall,
|
||||
createDjangoCSRClient,
|
||||
createDjangoSSRClient,
|
||||
ensureDjangoSession,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
10
packages/mizan-react/src/utils.ts
Normal file
10
packages/mizan-react/src/utils.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user