/** * 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 } /** * 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 | null, ): InvalidateEntry[] | null { if (!entry.affects) return null const result: InvalidateEntry[] = [] const seen = new Set() 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 = {} 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(', ') }