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>
This commit is contained in:
2026-04-07 00:20:32 -04:00
parent 7daec1c2e2
commit a2388b3ab2
9 changed files with 370 additions and 19 deletions

View File

@@ -102,6 +102,8 @@ export function client(optionsOrFn: ClientOptions | ClientOptions, fn?: Function
route: options.route,
methods: options.methods,
auth: options.auth,
rev: options.rev,
cache: options.cache,
}
register(entry)
@@ -132,6 +134,8 @@ export function client(optionsOrFn: ClientOptions | ClientOptions, fn?: Function
route: options.route,
methods: options.methods,
auth: options.auth,
rev: options.rev,
cache: options.cache,
}
register(entry)

View File

@@ -67,12 +67,34 @@ export async function handleContextFetch(
}
}
// Resolve effective cache policy (minimum TTL across all functions)
let effectiveCache: number | boolean = true
for (const fnName of fnNames) {
const entry = getFunction(fnName)
if (!entry) continue
if (entry.cache === false) { effectiveCache = false; break }
if (typeof entry.cache === 'number') {
effectiveCache = effectiveCache === true
? entry.cache
: Math.min(effectiveCache as number, entry.cache)
}
}
let cacheControl: string
if (effectiveCache === false) {
cacheControl = 'no-store'
} else if (typeof effectiveCache === 'number') {
cacheControl = `public, max-age=0, s-maxage=${effectiveCache}`
} else {
cacheControl = 'public, max-age=0, s-maxage=31536000'
}
return {
status: 200,
body: results,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=0, s-maxage=31536000',
'Cache-Control': cacheControl,
},
}
}

View File

@@ -35,6 +35,8 @@ export function generateManifest(baseUrl = '/api/mizan'): EdgeManifest {
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)
}

View File

@@ -17,6 +17,8 @@ export interface ClientOptions {
route?: string
methods?: string[]
auth?: boolean
rev?: number
cache?: number | false
}
export interface ParamDef {
@@ -36,6 +38,8 @@ export interface RegistryEntry {
route?: string
methods?: string[]
auth?: boolean
rev?: number
cache?: number | false
}
export interface ManifestContext {