packages/ flattens into: backends/ server protocol adapters (mizan-django, mizan-ts) frontends/ client kernel + framework adapters (mizan-base, mizan-react, mizan-vue, mizan-svelte) workers/ runtime workers (mizan-ssr) cores/ shared language-level primitives (empty for now; mizan-python forthcoming) The frontend kernel (was packages/mizan-runtime, now frontends/mizan-base) is renamed to reflect its role — it's the shared base that frontend adapters depend on directly. Reflects the substrate position that per-framework adapters wrap a single shared kernel; codegen targets the adapter, not the raw kernel. Path updates landed in: Makefile, two Gitea workflows, Dockerfile.test, four example/harness config files, .claude/settings.local.json, four docs (CLAUDE/ISSUES/ROADMAP/AFI_ARCHITECTURE), four codegen templates (stage1 + react/vue/svelte adapters), and three package.jsons (the mizan-base rename plus mizan-vue/svelte peerDeps). Generated files under examples/django-react-site/harness/src/api/ still reference @mizan/runtime — left as-is; they're regenerated artifacts and the harness is non-functional pending the React wrapper-layer codegen. Also folded in a pre-existing fix: the Gitea workflows had working-directory: react / django pointing at a layout that predates packages/, never updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
/**
|
|
* Invalidation protocol — header formatting, auto-scoping.
|
|
*
|
|
* Matches Django's implementation exactly. Same format. Same rules.
|
|
*/
|
|
|
|
import type { RegistryEntry } from './types'
|
|
import { getContextGroups, getContextParamNames, getFunction } from './registry'
|
|
|
|
type InvalidateEntry = string | { context: string; params: Record<string, any> }
|
|
|
|
/**
|
|
* Resolve invalidation targets with three-tier auto-scoping.
|
|
*
|
|
* Tier 1: Argument name matching
|
|
* Tier 2: Auth inference (Edge-side, not handled here)
|
|
* Tier 3: Broad fallback
|
|
*/
|
|
export function resolveInvalidation(
|
|
entry: RegistryEntry,
|
|
callArgs: Record<string, any> | null,
|
|
): InvalidateEntry[] | null {
|
|
if (!entry.affects) return null
|
|
|
|
const result: InvalidateEntry[] = []
|
|
const seen = new Set<string>()
|
|
|
|
for (const target of entry.affects) {
|
|
const targetName = target.name
|
|
if (seen.has(targetName)) continue
|
|
seen.add(targetName)
|
|
|
|
// Resolve which context the target belongs to (for param lookup)
|
|
const resolved = resolveAffectsTarget(targetName)
|
|
const ctxForParams = resolved.type === 'function' ? resolved.context : resolved.name
|
|
|
|
// Tier 1: argument name matching
|
|
if (callArgs && ctxForParams) {
|
|
const contextParams = getContextParamNames(ctxForParams)
|
|
const matched: Record<string, any> = {}
|
|
for (const [k, v] of Object.entries(callArgs)) {
|
|
if (contextParams.has(k)) matched[k] = v
|
|
}
|
|
if (Object.keys(matched).length > 0) {
|
|
result.push({ context: targetName, params: matched })
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Tier 3: broad fallback
|
|
result.push(targetName)
|
|
}
|
|
|
|
return result.length > 0 ? result : null
|
|
}
|
|
|
|
/**
|
|
* Determine whether an affects target is a context name or function name.
|
|
*/
|
|
function resolveAffectsTarget(name: string): { type: 'context' | 'function'; name: string; context?: string } {
|
|
const groups = getContextGroups()
|
|
|
|
if (name in groups) {
|
|
return { type: 'context', name }
|
|
}
|
|
|
|
for (const [ctxName, fnNames] of Object.entries(groups)) {
|
|
if (fnNames.includes(name)) {
|
|
return { type: 'function', name, context: ctxName }
|
|
}
|
|
}
|
|
|
|
return { type: 'context', name }
|
|
}
|
|
|
|
/**
|
|
* Format invalidation targets as X-Mizan-Invalidate header value.
|
|
*
|
|
* Format: comma-separated contexts. Semicolon-separated URL-encoded params.
|
|
*/
|
|
export function formatInvalidateHeader(invalidate: InvalidateEntry[]): string {
|
|
const parts: string[] = []
|
|
|
|
for (const entry of invalidate) {
|
|
if (typeof entry === 'string') {
|
|
parts.push(entry)
|
|
} else {
|
|
const { context, params } = entry
|
|
if (params && Object.keys(params).length > 0) {
|
|
const paramStr = Object.entries(params)
|
|
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
.map(([k, v]) => `${encodeURIComponent(String(k))}=${encodeURIComponent(String(v))}`)
|
|
.join(';')
|
|
parts.push(`${context};${paramStr}`)
|
|
} else {
|
|
parts.push(context)
|
|
}
|
|
}
|
|
}
|
|
|
|
return parts.join(', ')
|
|
}
|