Add named contexts, bundled fetch endpoint, and affects invalidation

Phase 1 (Named Contexts):
- @client(context=) accepts any string, not just 'global'/'local'
- context='local' emits deprecation warning
- Registry groups functions by context name (get_context_groups)
- GET /api/mizan/ctx/<name>/ bundles all context functions in one response
- Schema export includes x-mizan-contexts with param elevation metadata

Phase 2 (Affects):
- @client(affects=) declares mutation invalidation targets
- Accepts context name strings, function refs, or lists
- Mutually exclusive with context=
- Exported in x-mizan-functions schema for codegen

React runtime:
- MizanContextValue gains invalidateContext, invalidateFunctions,
  registerContextProvider, and baseUrl
- Named context providers register for invalidation on mount

259 Django tests pass, 33 React tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 22:47:55 -04:00
parent f3c225ef49
commit 3523f2e3fe
8 changed files with 572 additions and 56 deletions

View File

@@ -137,6 +137,34 @@ export interface MizanContextValue {
* (e.g., calling a server function immediately on mount).
*/
whenReady: Promise<void>
/**
* Invalidate a named context, triggering a refetch.
* Only refetches if the context is currently mounted (has a registered provider).
* No-op if the context is not mounted.
*/
invalidateContext: (name: string) => Promise<void>
/**
* Invalidate specific functions within their contexts.
* Groups by context and calls invalidateContext per group.
*/
invalidateFunctions: (names: string[]) => Promise<void>
/**
* Register a named context provider for invalidation support.
* Called by generated context providers on mount.
* Returns an unregister function (call on unmount).
*/
registerContextProvider: (
name: string,
refetch: () => Promise<void>,
) => () => void
/**
* Base URL for HTTP calls (for use by generated context providers).
*/
baseUrl: string
}
export interface MizanProviderProps {
@@ -466,6 +494,51 @@ export function MizanProvider({
const isRPCAvailable = status === 'connected'
// Named context provider registry for invalidation
const contextProvidersRef = useRef<Map<string, { refetch: () => Promise<void> }>>(new Map())
const registerContextProvider = useCallback(
(name: string, refetch: () => Promise<void>): (() => void) => {
contextProvidersRef.current.set(name, { refetch })
return () => {
contextProvidersRef.current.delete(name)
}
},
[]
)
const invalidateContext = useCallback(
async (name: string): Promise<void> => {
const provider = contextProvidersRef.current.get(name)
if (provider) {
await provider.refetch()
}
// If not mounted, no-op — no wasted request
},
[]
)
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)
}
}
await Promise.all(
Array.from(contexts).map(ctx => invalidateContext(ctx))
)
},
[invalidateContext]
)
const value = useMemo<MizanContextValue>(
() => ({
call,
@@ -477,8 +550,12 @@ export function MizanProvider({
onPush,
onContextChange,
whenReady: sessionRef.current!.promise,
invalidateContext,
invalidateFunctions,
registerContextProvider,
baseUrl,
}),
[call, getContext, refreshContext, refreshAllContexts, status, isRPCAvailable, onPush, onContextChange]
[call, getContext, refreshContext, refreshAllContexts, status, isRPCAvailable, onPush, onContextChange, invalidateContext, invalidateFunctions, registerContextProvider, baseUrl]
)
return (