Add mizan-ts: TypeScript backend adapter proving AFI is language-agnostic
The TypeScript adapter produces the same manifest, the same X-Mizan-Invalidate headers, the same JSON invalidation protocol, and the same CDN-ready response headers as mizan-django. One Edge Worker. Two backend languages. Same protocol. Features: - @client decorator (function wrapper + class method decorator) - ReactContext class (same API as Django adapter) - Registry with context groups and param tracking - Context bundled GET: /api/mizan/ctx/<name>/ - Mutation POST: /api/mizan/call/ with server-driven invalidation - Three-tier auto-scoping (argument name matching → broad fallback) - Function-level affects targeting - private=True (rejected from RPC, in manifest for Edge) - X-Mizan-Invalidate header with URL-encoded params - Edge manifest generation (identical format to Django's) - render_strategy + user_scoped derivation 22 edge compatibility tests pass (Bun, 21ms): - Deterministic JSON, sorted keys - Cache-Control: public on GETs, no-store on mutations/errors - Vary: Authorization, Cookie - Header round-trip with special characters - Auto-scoped invalidation matches body and header - Function-level invalidation - Private function rejection - Manifest structure with PSR/dynamic_cached strategies Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
89
packages/mizan-ts/src/manifest.ts
Normal file
89
packages/mizan-ts/src/manifest.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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'
|
||||
|
||||
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 = { 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user