Port of Python's origin-side cache to TypeScript: - cache/keys.ts: deriveCacheKey with stableStringify for JSON-canonical HMAC - cache/backend.ts: MemoryCache (same API as Python) - cache/index.ts: cacheGet, cachePut, cachePurge with AND semantics Integrated into dispatch.ts: - handleContextFetch: cache lookup before execution, store after - handleMutationCall: purge on invalidation Cross-language pin test proves Python and TypeScript produce identical HMAC-SHA256 output for the same inputs: Public: 605a1ca5ad5994e9b765c8d1b330474c2a0d51a7b8fbbdc402f992da7ba902f6 User-scoped: 30fc08eb46ee4ff2cf7d317e97dca90fd616511e0587304416f71dc863338dc2 34 TypeScript tests (9 new), 165 Python tests (1 new pin test). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
101 lines
2.9 KiB
TypeScript
101 lines
2.9 KiB
TypeScript
/**
|
|
* mizan cache — TypeScript adapter.
|
|
*
|
|
* Same protocol as Python's mizan.cache. Cross-language conformance
|
|
* verified by pin tests.
|
|
*/
|
|
|
|
export { MemoryCache } from './backend'
|
|
export type { CacheBackend } from './backend'
|
|
export { deriveCacheKey, buildIndexKeys } from './keys'
|
|
|
|
import type { CacheBackend } from './backend'
|
|
import { deriveCacheKey, buildIndexKeys } from './keys'
|
|
|
|
let _cacheInstance: CacheBackend | null = null
|
|
|
|
export function getCache(): CacheBackend | null {
|
|
return _cacheInstance
|
|
}
|
|
|
|
export function setCache(backend: CacheBackend | null): void {
|
|
_cacheInstance = backend
|
|
}
|
|
|
|
export function resetCache(): void {
|
|
_cacheInstance = null
|
|
}
|
|
|
|
export function cacheGet(
|
|
secret: string,
|
|
backend: CacheBackend,
|
|
context: string,
|
|
params: Record<string, any>,
|
|
userId?: string,
|
|
rev: number = 0,
|
|
): string | null {
|
|
const key = deriveCacheKey(secret, context, params, userId, rev)
|
|
return backend.get(key)
|
|
}
|
|
|
|
export function cachePut(
|
|
secret: string,
|
|
backend: CacheBackend,
|
|
context: string,
|
|
params: Record<string, any>,
|
|
value: string,
|
|
userId?: string,
|
|
rev: number = 0,
|
|
): void {
|
|
const key = deriveCacheKey(secret, context, params, userId, rev)
|
|
const indexes = buildIndexKeys(context, params)
|
|
backend.put(key, value, indexes)
|
|
}
|
|
|
|
export function cachePurge(
|
|
backend: CacheBackend,
|
|
context: string,
|
|
params?: Record<string, any> | null,
|
|
): number {
|
|
if (params) {
|
|
// Scoped purge — AND semantics (intersection)
|
|
const setsPerParam: Set<string>[] = []
|
|
const paramIndexKeys: string[] = []
|
|
for (const [k, v] of Object.entries(params).sort(([a], [b]) => a.localeCompare(b))) {
|
|
const indexKey = `mizan:idx:${context}:${k}=${String(v)}`
|
|
paramIndexKeys.push(indexKey)
|
|
setsPerParam.push(backend.getIndex(indexKey))
|
|
}
|
|
|
|
let keysToDelete: Set<string>
|
|
if (setsPerParam.length > 0) {
|
|
keysToDelete = setsPerParam[0]
|
|
for (let i = 1; i < setsPerParam.length; i++) {
|
|
keysToDelete = new Set([...keysToDelete].filter(k => setsPerParam[i].has(k)))
|
|
}
|
|
} else {
|
|
keysToDelete = new Set()
|
|
}
|
|
|
|
if (keysToDelete.size > 0) {
|
|
for (const idxKey of paramIndexKeys) {
|
|
backend.removeFromIndex(idxKey, keysToDelete)
|
|
}
|
|
backend.removeFromIndex(`mizan:idx:${context}`, keysToDelete)
|
|
return backend.deleteMany([...keysToDelete])
|
|
}
|
|
return 0
|
|
} else {
|
|
// Broad purge
|
|
const indexKey = `mizan:idx:${context}`
|
|
const keysToDelete = backend.getIndex(indexKey)
|
|
backend.deleteIndex(indexKey)
|
|
backend.deleteIndexesByPrefix(`mizan:idx:${context}:`)
|
|
|
|
if (keysToDelete.size > 0) {
|
|
return backend.deleteMany([...keysToDelete])
|
|
}
|
|
return 0
|
|
}
|
|
}
|