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
|
// Internal Utilities
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
function getCookie(name: string): string | null {
|
import { getCSRFToken } from '../utils'
|
||||||
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')
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RequestBuild {
|
interface RequestBuild {
|
||||||
request: RequestInit
|
request: RequestInit
|
||||||
@@ -537,49 +525,3 @@ export interface FunctionSuccessResponse<T> {
|
|||||||
*/
|
*/
|
||||||
export type FunctionResponse<T> = FunctionSuccessResponse<T> | FunctionErrorResponse
|
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 { useJWT } from './jwt'
|
||||||
import { DjangoError, type ErrorCode, type FunctionErrorResponse } from './errors'
|
import { DjangoError, type ErrorCode, type FunctionErrorResponse } from './errors'
|
||||||
|
|
||||||
// ============================================================================
|
import { getCSRFToken } from './utils'
|
||||||
// Utilities
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function getCSRFToken(): string | null {
|
|
||||||
if (typeof document === 'undefined') return null
|
|
||||||
const match = document.cookie.match(/csrftoken=([^;]+)/)
|
|
||||||
return match?.[1] ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@@ -546,18 +538,10 @@ export function MizanProvider({
|
|||||||
|
|
||||||
const invalidateFunctions = useCallback(
|
const invalidateFunctions = useCallback(
|
||||||
async (names: string[]): Promise<void> => {
|
async (names: string[]): Promise<void> => {
|
||||||
// Each function belongs to a context. Invalidating a function
|
// Function names are passed directly as context invalidation targets.
|
||||||
// means refetching its entire context (since the bundling endpoint
|
// The server already resolved function → context mapping.
|
||||||
// returns all functions). Dedupe by context name.
|
// Dedupe and invalidate each.
|
||||||
const contexts = new Set<string>()
|
const contexts = new Set(names)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(contexts).map(ctx => invalidateContext(ctx))
|
Array.from(contexts).map(ctx => invalidateContext(ctx))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ export {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export {
|
export {
|
||||||
httpFunctionCall,
|
|
||||||
createDjangoCSRClient,
|
createDjangoCSRClient,
|
||||||
createDjangoSSRClient,
|
createDjangoSSRClient,
|
||||||
ensureDjangoSession,
|
ensureDjangoSession,
|
||||||
|
|||||||
@@ -10,12 +10,7 @@ import {
|
|||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import type { JWTTokens, JWTConfig, JWTState } from '../client/types'
|
import type { JWTTokens, JWTConfig, JWTState } from '../client/types'
|
||||||
|
import { getCSRFToken } from '../utils'
|
||||||
function getCSRFToken(): string | null {
|
|
||||||
if (typeof document === 'undefined') return null
|
|
||||||
const match = document.cookie.match(/csrftoken=([^;]+)/)
|
|
||||||
return match?.[1] ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
const Context = createContext<JWTState | null>(null)
|
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