Files
mizan/backends/mizan-ts/src/invalidation.ts
Ryth Azhur fe39fcb229 Restructure tree by role; rename mizan-runtime → mizan-base
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>
2026-05-05 20:55:37 -04:00

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(', ')
}