@samveys/cache
Dari packages/cache/README.md di repository framework.
Halaman ini belum diterjemahkan. Kamu sedang membaca versi bahasa Inggris.
Cache stores, cross-process locks and a concurrency funnel. No runtime dependencies.
| Store | What it is | Use for |
|---|---|---|
memoryStore() | A Map with expiry; values are structured-cloned | Tests, single process |
databaseStore({ database, table? }) | A SQL table (key, JSON value, expires_at ms), created on first use | Shared state, cross-process locks |
failoverStore(['a', 'b']) | Tries stores in order; all failing is SVS_E097 (503) | A fallback when a store is down |
TTLs are in milliseconds. A missing TTL means "never expires".
Set up
import { cache, databaseStore, defineCacheConfig, failoverStore, memoryStore } from '@samveys/cache'
import { createDatabaseManager, defineDatabaseConfig, sqlite } from '@samveys/database'
const db = createDatabaseManager(defineDatabaseConfig({ default: 'main', connections: { main: sqlite({ file: ':memory:' }) } }))
export const config = defineCacheConfig({
default: 'shared',
stores: {
shared: failoverStore(['database', 'memory']),
database: databaseStore({ database: () => db, table: 'samveys_cache' }),
memory: memoryStore(),
},
})
export const plugin = cache(config)The plugin binds 'cache' (a CacheManager). Stores open on first use and close at shutdown.
Read, write, remember
import { createCacheManager, defineCacheConfig, memoryStore } from '@samveys/cache'
const cache = createCacheManager(defineCacheConfig({ default: 'm', stores: { m: memoryStore() } }))
await cache.put('user:1', { name: 'Ana' }, 60_000)
const user = await cache.get<{ name: string }>('user:1')
// Computes only on a miss. Concurrent callers on a miss each compute.
const report = await cache.remember('report', 5 * 60_000, async () => ({ rows: 42 }))
// Atomic counter; the TTL applies when the counter is created (a fixed window).
const hits = await cache.increment('hits', 1, 60_000)
console.log(user?.name, report.rows, hits)Locks
cache.lock(name, ttlMs) returns a handle with its own random owner token. release() only works
for the handle that acquired the lock, so a slow job whose lock expired cannot free a newer owner's lock.
import { createCacheManager, defineCacheConfig, memoryStore } from '@samveys/cache'
const cache = createCacheManager(defineCacheConfig({ default: 'm', stores: { m: memoryStore() } }))
const lock = cache.lock('nightly-report', 30_000)
if (await lock.acquire()) {
try {
// only one holder at a time
} finally {
await lock.release()
}
}
// Or wait up to 5 s; throws SVS_E096 (409) when the wait runs out.
const other = cache.lock('nightly-report', 30_000)
await other.block(5_000)
await other.release()With the database store, the lock is an INSERT into a primary-key column, so it holds
across processes and servers that share the database: the second INSERT fails with a unique
violation and acquire() returns false. A lock whose TTL passed is taken over by the next caller.
Funnel
A funnel limits how many callers run at once, across every process on the same store.
import { createCacheManager, defineCacheConfig, memoryStore } from '@samveys/cache'
const cache = createCacheManager(defineCacheConfig({ default: 'm', stores: { m: memoryStore() } }))
const funnel = cache.funnel('thumbnails', { limit: 3, ttlMs: 60_000 })
// Waits up to 2 s for a free slot, else SVS_E096. The slot is freed even when the function throws.
const result = await funnel.run(async () => 'resized', { waitMs: 2_000 })
console.log(result)ttlMs frees a slot whose holder crashed.
Notes
memoryStore()is per process: usedatabaseStore()for locks and counters that must be shared.- The database store keeps expired rows until they are read or replaced; delete old rows with a scheduled
DELETE … WHERE expires_at < now. - Cache values must be JSON-compatible in the database store.