first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View 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.