first commit
This commit is contained in:
86
docs/api-stack/api/api-tenant/src/cache/cache.md
vendored
Normal file
86
docs/api-stack/api/api-tenant/src/cache/cache.md
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
# cache
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `cache` module wraps any `TenantResolver` with a bounded, TTL-aware
|
||||
in-process cache that prevents lookup stampedes. `TenantCache.wrap(resolver,
|
||||
options)` returns a new `TenantResolver` that holds positive entries for
|
||||
`ttlMs`, negative entries (no-tenant) for `negativeTtlMs`, evicts the
|
||||
oldest entry when the bounded `maxEntries` cap is hit, and coalesces
|
||||
concurrent in-flight lookups for the same key into a single `Promise` so
|
||||
a burst of requests hitting an unmatched key fires `lookupFn` exactly
|
||||
once.
|
||||
|
||||
The cache is intentionally in-process and bounded — the design trades a
|
||||
distributed-cache hit-rate for predictable memory + zero deployment
|
||||
state. Callers wanting Redis or another shared store can keep
|
||||
`TenantCache.wrap` in front of a custom resolver that reads from there;
|
||||
the cache wrapper is a separable concern from the persistence layer.
|
||||
|
||||
## Features
|
||||
|
||||
- TTL for positive entries (`ttlMs`) and negative entries
|
||||
(`negativeTtlMs`, defaults to `ttlMs / 4`).
|
||||
- Bounded by `maxEntries` (default 1000), oldest-first eviction
|
||||
using the natural insertion-order semantics of `Map`.
|
||||
- In-flight request coalescing via an internal `Map<key, Promise>` so
|
||||
100 parallel hits for the same key fire `lookupFn` once.
|
||||
- Pluggable `keyFn(c)` — defaults to `host + path`, falling back to
|
||||
`x-tenant-id` header, finally to bare path. Override per app.
|
||||
- Injectable `clock` — `FakeClock` from `@suaveplan/testing` works.
|
||||
- LRU touch-on-hit — accessing a cached entry re-inserts it so the
|
||||
oldest-evicted entry is the genuinely least-recently used one.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { TenantCache, headerResolver } from "@suaveplan/api-tenant";
|
||||
|
||||
const inner = headerResolver("x-tenant-id", (id) => db.tenants.get(id));
|
||||
const cached = TenantCache.wrap(inner, { ttlMs: 30_000 });
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Pinning a deterministic clock and overriding the key derivation for a
|
||||
subdomain resolver:
|
||||
|
||||
```ts
|
||||
import { TenantCache, subdomainResolver } from "@suaveplan/api-tenant";
|
||||
import { FakeClock } from "@suaveplan/testing";
|
||||
|
||||
const clock = new FakeClock(0);
|
||||
const cached = TenantCache.wrap(
|
||||
subdomainResolver("app.com", (slug) => db.tenants.bySlug(slug)),
|
||||
{
|
||||
ttlMs: 60_000,
|
||||
negativeTtlMs: 5_000,
|
||||
maxEntries: 5_000,
|
||||
keyFn: (c) => `sub:${c.req.header("host") ?? ""}`,
|
||||
clock: () => clock.now(),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class TenantCache` — static `.wrap(resolver, options): TenantResolver`.
|
||||
- `class CachedTenantResolver` — concrete class, exposes `.size` and
|
||||
`.clear()` for tests; not normally constructed directly.
|
||||
- `interface TenantCacheOptions` — `ttlMs` (required), `negativeTtlMs?`,
|
||||
`maxEntries?`, `keyFn?`, `clock?`.
|
||||
- `type CacheClock = () => number`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Map` preserves insertion order in JavaScript, so the "oldest" entry
|
||||
during eviction is the first one yielded by `.keys().next()`. A cache
|
||||
hit deletes-and-reinserts the entry, promoting it to the most-recent
|
||||
position — this is full LRU behaviour without a doubly-linked list.
|
||||
The negative TTL is shorter than the positive TTL by default so a
|
||||
mistyped tenant slug self-corrects faster than a real one becomes
|
||||
stale.
|
||||
Reference in New Issue
Block a user