Files
mizan/packages/mizan-ts/src/manifest.ts
Ryth Azhur a2388b3ab2 Add rev and cache parameters to @client decorator
rev=N: bumped by developer when function logic changes. Becomes part of
the HMAC cache key — old cache entries are unreachable without purge.
Effective rev for a context is max(rev) across all functions in it.

cache=int|False|True: TTL escape hatch for unobservable mutations.
cache=60 emits s-maxage=60. cache=False emits no-store. Default (True)
emits s-maxage=31536000 (forever, purge on mutation).
Effective cache for a context is min(TTL) across functions, with False
taking precedence.

Both parameters flow through: decorator → meta → manifest → cache key
and Cache-Control headers. Implemented in both Python and TypeScript
with 13 Python tests and 4 TypeScript tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 00:20:32 -04:00

94 lines
3.3 KiB
TypeScript

/**
* Edge Manifest Generator
*
* Produces the same JSON format as mizan-django. One Edge Worker.
* Two backend languages. Same manifest.
*/
import type { EdgeManifest } from './types'
import { getAllFunctions, getContextGroups, getContextParamNames } from './registry'
// Both camelCase and snake_case forms included for cross-language matching.
// Wire format is snake_case (protocol rule); camelCase is the TS-local convention.
const USER_SCOPED_PARAMS = new Set(['userId', 'user', 'ownerId', 'accountId', 'user_id', 'owner_id', 'account_id'])
export function generateManifest(baseUrl = '/api/mizan'): EdgeManifest {
const groups = getContextGroups()
const allFunctions = getAllFunctions()
const manifest: EdgeManifest = { version: 1, contexts: {}, mutations: {} }
// Contexts
for (const [ctxName, fnNames] of Object.entries(groups)) {
const paramNames = new Set<string>()
const functions: Array<{ name: string; path: 'rpc' | 'view'; route?: string; methods?: string[] }> = []
const pageRoutes: string[] = []
for (const fnName of fnNames) {
const entry = allFunctions.get(fnName)
if (!entry) continue
for (const p of entry.params) paramNames.add(p.name)
const fnEntry: any = { name: fnName, path: entry.viewPath ? 'view' : 'rpc' }
if (entry.route) {
fnEntry.route = entry.route
fnEntry.methods = entry.methods || ['GET']
pageRoutes.push(entry.route)
}
if (entry.rev !== undefined && entry.rev !== 0) fnEntry.rev = entry.rev
if (entry.cache !== undefined && entry.cache !== true) fnEntry.cache = entry.cache
functions.push(fnEntry)
}
const sortedParams = [...paramNames].sort()
const userScoped = [...paramNames].some(p => USER_SCOPED_PARAMS.has(p))
const ctxEntry: EdgeManifest['contexts'][string] = {
functions,
endpoints: [`${baseUrl}/ctx/${ctxName}/`],
params: sortedParams,
user_scoped: userScoped,
render_strategy: userScoped ? 'dynamic_cached' : 'psr',
}
if (pageRoutes.length > 0) {
ctxEntry.page_routes = pageRoutes
}
manifest.contexts[ctxName] = ctxEntry
}
// Mutations
for (const [fnName, entry] of allFunctions) {
if (!entry.affects) continue
const affectedContexts = [...new Set(entry.affects.map(a => a.name))]
// Auto-scoped params
const fnParamNames = new Set(entry.params.map(p => p.name))
const autoScoped: string[] = []
for (const ctxName of affectedContexts) {
const ctxParams = getContextParamNames(ctxName)
for (const p of fnParamNames) {
if (ctxParams.has(p) && !autoScoped.includes(p)) {
autoScoped.push(p)
}
}
}
const mutation: EdgeManifest['mutations'][string] = {
affects: affectedContexts,
}
if (autoScoped.length > 0) mutation.auto_scoped_params = autoScoped.sort()
if (entry.private) mutation.private = true
if (entry.route) {
mutation.route = entry.route
mutation.methods = entry.methods || ['POST']
}
manifest.mutations[fnName] = mutation
}
return manifest
}