Add TypeScript cache adapter with cross-language conformance tests
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>
This commit is contained in:
100
packages/mizan-ts/src/cache/index.ts
vendored
Normal file
100
packages/mizan-ts/src/cache/index.ts
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user