Fix protocol mismatch + add SSR hydration to codegen
Three bugs fixed:
1. MizanProvider.call() read data.data but server returns data.result.
Now reads data.result and processes data.invalidate for server-driven
invalidation (triggering refetch on mounted context providers).
2. GlobalContextLoader expected {error, data} wrapper but context GET
returns raw bundled data. Fixed to iterate response directly.
3. Named context providers had same wrapper assumption. Fixed to
setData(result) directly.
Two features added:
1. SSR hydration: GlobalContextLoader checks window.__MIZAN_SSR_DATA__
on mount. If present, populates contexts from it and skips fetch.
2. SSR hydration: Named context providers check __MIZAN_SSR_DATA__ in
useState initializer. If SSR data exists for their functions, they
render immediately without fetching.
3. Server-driven invalidation in MizanProvider.call(): reads the
invalidate array from mutation responses and triggers refetch on
mounted providers. Generated mutation hooks' hardcoded invalidation
is now redundant but idempotent — both paths coexist safely.
Also fixed FunctionSuccessResponse type to match new protocol:
{ result: T, invalidate?: [...] }
373 Django + 33 React tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -268,15 +268,22 @@ export function generateMizanProvider(schema, options = {}) {
|
||||
lines.push(' if (loaded.current) return')
|
||||
lines.push(' loaded.current = true')
|
||||
lines.push('')
|
||||
lines.push(' // Check for SSR hydration data first')
|
||||
lines.push(" const ssr = typeof window !== 'undefined' && (window as any).__MIZAN_SSR_DATA__")
|
||||
lines.push(' if (ssr) {')
|
||||
lines.push(' for (const [name, data] of Object.entries(ssr)) {')
|
||||
lines.push(' mizan.setContextData(name, data)')
|
||||
lines.push(' }')
|
||||
lines.push(' return')
|
||||
lines.push(' }')
|
||||
lines.push('')
|
||||
lines.push(' ;(async () => {')
|
||||
lines.push(' await mizan.whenReady')
|
||||
lines.push(' try {')
|
||||
lines.push(" const response = await mizan.request('GET', `${mizan.baseUrl}/ctx/global/`)")
|
||||
lines.push(' const result = await response.json()')
|
||||
lines.push(' if (!result.error) {')
|
||||
lines.push(' for (const [name, data] of Object.entries(result.data)) {')
|
||||
lines.push(' mizan.setContextData(name, data)')
|
||||
lines.push(' }')
|
||||
lines.push(' for (const [name, data] of Object.entries(result)) {')
|
||||
lines.push(' mizan.setContextData(name, data)')
|
||||
lines.push(' }')
|
||||
lines.push(' } catch (e) {')
|
||||
lines.push(" console.error('[MizanContext] Global context fetch failed:', e)")
|
||||
@@ -456,11 +463,25 @@ export function generateMizanProvider(schema, options = {}) {
|
||||
// Provider component
|
||||
lines.push(`export function ${ctxPascal}Context({ children, ...params }: ${ctxPascal}ContextProps) {`)
|
||||
lines.push(` const mizan = useMizan()`)
|
||||
|
||||
// SSR hydration check — initialize from __MIZAN_SSR_DATA__ if available
|
||||
lines.push(` const [data, setData] = useState<{`)
|
||||
for (const fn of ctxFunctions) {
|
||||
lines.push(` ${fn.name}: ${fn.outputType}`)
|
||||
}
|
||||
lines.push(` } | null>(null)`)
|
||||
lines.push(` } | null>(() => {`)
|
||||
lines.push(` if (typeof window === 'undefined') return null`)
|
||||
lines.push(` const ssr = (window as any).__MIZAN_SSR_DATA__`)
|
||||
lines.push(` if (!ssr) return null`)
|
||||
// Check if all functions for this context have SSR data
|
||||
const firstFn = ctxFunctions[0]
|
||||
lines.push(` if (ssr.${firstFn.name} === undefined) return null`)
|
||||
lines.push(` return {`)
|
||||
for (const fn of ctxFunctions) {
|
||||
lines.push(` ${fn.name}: ssr.${fn.name},`)
|
||||
}
|
||||
lines.push(` }`)
|
||||
lines.push(` })`)
|
||||
lines.push('')
|
||||
lines.push(` const refetch = useCallback(async () => {`)
|
||||
lines.push(` await mizan.whenReady`)
|
||||
@@ -470,7 +491,7 @@ export function generateMizanProvider(schema, options = {}) {
|
||||
}
|
||||
lines.push(` const resp = await mizan.request('GET', \`\${mizan.baseUrl}/ctx/${ctxName}/?\${qs}\`)`)
|
||||
lines.push(` const result = await resp.json()`)
|
||||
lines.push(` if (!result.error) setData(result.data)`)
|
||||
lines.push(` setData(result)`)
|
||||
|
||||
// Dependency array: mizan + each param
|
||||
const deps = ['mizan', ...paramEntries.map(([pName]) => `params.${pName}`)]
|
||||
@@ -650,13 +671,13 @@ export function generateMizanServer(schema) {
|
||||
lines.push('')
|
||||
lines.push(' try {')
|
||||
lines.push(" const response = await client.request('GET', '/api/mizan/ctx/global/')")
|
||||
lines.push(' const result = await response.json()')
|
||||
lines.push(' if (!result.error) {')
|
||||
lines.push(' if (response.ok) {')
|
||||
lines.push(' const result = await response.json()')
|
||||
for (const ctx of globalContexts) {
|
||||
lines.push(` if (result.data?.${ctx.name} !== undefined) hydration.${ctx.camelName} = result.data.${ctx.name}`)
|
||||
lines.push(` if (result?.${ctx.name} !== undefined) hydration.${ctx.camelName} = result.${ctx.name}`)
|
||||
}
|
||||
lines.push(' } else {')
|
||||
lines.push(" console.error('[getMizanHydration] Global context fetch failed:', result.code, result.message)")
|
||||
lines.push(" console.error('[getMizanHydration] Global context fetch failed:', response.status)")
|
||||
lines.push(' }')
|
||||
lines.push(' } catch (e) {')
|
||||
lines.push(" console.error('[getMizanHydration] Request failed:', e)")
|
||||
|
||||
@@ -528,8 +528,8 @@ import { DjangoError, type FunctionErrorResponse } from '../errors'
|
||||
* Success response from a server function
|
||||
*/
|
||||
export interface FunctionSuccessResponse<T> {
|
||||
error: false
|
||||
data: T
|
||||
result: T
|
||||
invalidate?: Array<string | { context: string; params: Record<string, any> }>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -580,6 +580,6 @@ export async function httpFunctionCall<TInput = unknown, TOutput = unknown>(
|
||||
throw new DjangoError(data)
|
||||
}
|
||||
|
||||
return data.data
|
||||
return (data as FunctionSuccessResponse<TOutput>).result
|
||||
}
|
||||
|
||||
|
||||
@@ -343,13 +343,26 @@ export function MizanProvider({
|
||||
{ fn: functionName, args: input }
|
||||
)
|
||||
|
||||
const data: FunctionResponse<TOutput> = await response.json()
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new DjangoError(data as FunctionErrorResponse)
|
||||
}
|
||||
|
||||
return data.data
|
||||
// Server-driven invalidation: process the invalidate array
|
||||
if (data.invalidate && Array.isArray(data.invalidate)) {
|
||||
for (const entry of data.invalidate) {
|
||||
if (typeof entry === 'string') {
|
||||
const provider = contextProvidersRef.current.get(entry)
|
||||
if (provider) provider.refetch()
|
||||
} else if (entry.context) {
|
||||
const provider = contextProvidersRef.current.get(entry.context)
|
||||
if (provider) provider.refetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data.result as TOutput
|
||||
},
|
||||
[connection, baseUrl, httpClient]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user