New:Socket for Asana Is Now Available.Learn more
Get Started

@ultimat3/cache

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/cache - npm Package Compare versions

Comparing version
16.0.0
to
17.0.0
+14
-0
CLAUDE.md

@@ -13,2 +13,16 @@ # @ultimat3/cache — agent notes

- **A tier's own LIMIT is screened where `app.config.ts` names it, `As of 2026-08-26`** —
`assertFiniteCapacity` for a byte budget or an entry ceiling, `assertFiniteDurationMs` for a
request budget or a `defaultTtlMs`, `assertFiniteSimilarityFloor` for the semantic tier's
threshold — all three beside `assertTtl` in `tiers.ts`, and all three raising one code,
`X_CACHE_LIMIT_INVALID`. Not a nicety: `Number(process.env.CACHE_MAX_BYTES)` on an unset variable
is `NaN`, `NaN` is not nullish so `??` keeps it, and every comparison against it is FALSE —
`while (bytes > maxBytes)` never evicted, so the bounded cache this package is built on had no
bound at all, and `similarity < floor` never skipped, so the semantic tier answered the nearest
thing it held to a question nobody asked. The failure is never a wrong number; it is the guard
switching itself off. Never `Math.max(1, x)` in front of one: that is a clamp, not a validator.
**The `Finite` in all three names is load-bearing** — `bun run finite-bounds` recognises a repair
by the shape of the CALL, so while they were spelled `assertCapacity` / `assertTimeoutMs` all
nine of this package's options read as unchecked to the ratchet while every one was screened.
- `invalidateTags()` in `invalidate.ts` is the ONLY fan-out path. Never call

@@ -15,0 +29,0 @@ `tier.invalidateTags()` from outside it. It is also the only place the log is written:

+2
-2
{
"name": "@ultimat3/cache",
"version": "16.0.0",
"version": "17.0.0",
"description": "Tagged caching: request memo, LRU, Redis, CDN — one invalidation graph",

@@ -34,4 +34,4 @@ "license": "MIT",

"dependencies": {
"@ultimat3/core": "16.0.0"
"@ultimat3/core": "17.0.0"
}
}

@@ -9,2 +9,3 @@ // The X_* codes owned by @ultimat3/cache. Each one names the exact config change or

'X_CACHE_JITTER_INVALID',
'X_CACHE_LIMIT_INVALID',
'X_CACHE_PURGE_FAILED',

@@ -25,2 +26,3 @@ 'X_CACHE_TAG_UNKNOWN',

X_CACHE_JITTER_INVALID: 'a TTL jitter fraction outside [0, 1)',
X_CACHE_LIMIT_INVALID: 'a cache ceiling, similarity floor or timeout that is not a usable number',
X_CACHE_PURGE_FAILED: 'the CDN refused a purge',

@@ -77,2 +79,52 @@ X_CACHE_TAG_UNKNOWN: 'a tag no entity declared',

/**
* A LIMIT this package runs on that is not one — a tier's byte budget, its entry ceiling, the
* similarity floor the semantic tier decides "same question" with, or a purge driver's request
* timeout. Sibling of the TTL refusal below, over the knobs an `app.config.ts` carries and
* `CacheTooLargeError`'s `fix:` line already tells an operator to raise.
*
* `Number(process.env.CACHE_MAX_BYTES)` on an unset variable is `NaN`, `NaN` is not nullish so
* `??` passes it through, and every comparison it reaches then answers false. The failure is never
* a wrong number, it is the guard turning itself off — `while (bytes > NaN)` never evicts, so the
* cache has no ceiling at all, and `similarity < NaN` never skips, so a semantic lookup answers
* the nearest thing it holds to a question nobody asked. Refused where the value is still
* nameable, exactly as `assertTtl` does.
*/
/**
* The mechanism is stated ONLY for `NaN`, and that is deliberate. This cause used to say "a
* comparison against this one is false for every entry" for every rejected value, which is true of
* `NaN` and false of everything else it also receives: `Infinity`, `-Infinity`, `0`, a fraction, an
* unsafe integer. A ceiling of `Infinity` is never *exceeded* and a similarity floor of `Infinity`
* is never *met* — opposite outcomes, from one sentence claiming both. A cause that names the wrong
* failure sends the reader to the wrong place, which is the failure mode axiom 4 exists to prevent,
* so the shared half says only what is true of every value and the `NaN` half is added when it is.
*/
const limitEffect = (value: number): string =>
Number.isNaN(value)
? ', and every comparison against NaN is false, so the limit stops being enforced rather than being enforced wrongly — NaN is what Number(process.env.…) answers for an unset variable, and it is not nullish, so `??` does not catch it'
: ', and it is applied as given, so the tier is bounded at a value nobody chose';
export class CacheLimitInvalidError extends UltimateError {
constructor(input: {
option: string;
value: number;
tier: string;
expected: string;
/**
* Where the value came FROM, when that is not an `app.config.ts` key. `loadDeadlineMs` and the
* similarity override arrive as call arguments (`createCacheStack`, `lookup`), so telling their
* caller to edit `app.config.ts` names a key that does not exist.
*/
source?: string | undefined;
}) {
const where = input.source ?? `${input.tier}.${input.option} in app.config.ts`;
super({
code: 'X_CACHE_LIMIT_INVALID',
cause: `the ${input.tier} tier was given ${input.option}=${String(input.value)}; it must be ${input.expected}${limitEffect(input.value)}`,
fix: `set ${where} to ${input.expected} — and if it comes from the environment, parse it first: Number(process.env.CACHE_LIMIT) is NaN when the variable is unset`,
meta: { option: input.option, value: String(input.value), tier: input.tier },
});
}
}
/**
* A `ttlMs` that is not a positive, finite number of milliseconds.

@@ -79,0 +131,0 @@ *

@@ -11,2 +11,3 @@ // Public API of @ultimat3/cache. Explicit, no `export *`.

CacheJitterInvalidError,
CacheLimitInvalidError,
CachePurgeFailedError,

@@ -113,2 +114,5 @@ CacheTagUnknownError,

export {
assertFiniteCapacity,
assertFiniteDurationMs,
assertFiniteSimilarityFloor,
assertTtl,

@@ -115,0 +119,0 @@ createCacheStack,

@@ -19,3 +19,3 @@ // Tier 1: in-process LRU bounded by BYTES, not entry count — an entry count budget is a

} from './tiers';
import { assertTtl, nowMs } from './tiers';
import { assertFiniteCapacity, assertFiniteDurationMs, assertTtl, nowMs } from './tiers';

@@ -85,2 +85,5 @@ export interface LruOptions {

/** 64 MiB. Named because the constructor now screens it, and a screen needs something to pass. */
const DEFAULT_LRU_MAX_BYTES = 64 * 1024 * 1024;
export class LruCache {

@@ -102,4 +105,12 @@ private readonly map = new Map<string, LruNode>();

constructor(options: LruOptions = {}) {
this.maxBytes = options.maxBytes ?? 64 * 1024 * 1024;
this.defaultTtlMs = options.defaultTtlMs ?? 60_000;
this.maxBytes = assertFiniteCapacity(
'lru',
'maxBytes',
options.maxBytes ?? DEFAULT_LRU_MAX_BYTES,
);
this.defaultTtlMs = assertFiniteDurationMs(
'lru',
'defaultTtlMs',
options.defaultTtlMs ?? 60_000,
);
this.clock = options.clock ?? systemClock;

@@ -106,0 +117,0 @@ this.jitter = {

@@ -21,2 +21,3 @@ // Single responsibility: Cloudflare's cache-tag purge. One `POST /zones/<id>/purge_cache` per

} from './purge-http';
import { assertFiniteDurationMs } from './tiers';

@@ -77,3 +78,7 @@ export const CLOUDFLARE_API_URL = 'https://api.cloudflare.com/client/v4';

const baseUrl = options.baseUrl ?? CLOUDFLARE_API_URL;
const timeoutMs = options.timeoutMs ?? DEFAULT_PURGE_TIMEOUT_MS;
const timeoutMs = assertFiniteDurationMs(
'cloudflare',
'timeoutMs',
options.timeoutMs ?? DEFAULT_PURGE_TIMEOUT_MS,
);
const doFetch = options.fetch ?? defaultPurgeFetch;

@@ -80,0 +85,0 @@ const headers = { Authorization: `Bearer ${apiToken}` };

@@ -22,2 +22,3 @@ // Single responsibility: Fastly's surrogate-key purge. One `POST /service/<id>/purge` per batch

} from './purge-http';
import { assertFiniteDurationMs } from './tiers';

@@ -76,3 +77,7 @@ export const FASTLY_API_URL = 'https://api.fastly.com';

const baseUrl = options.baseUrl ?? FASTLY_API_URL;
const timeoutMs = options.timeoutMs ?? DEFAULT_PURGE_TIMEOUT_MS;
const timeoutMs = assertFiniteDurationMs(
'fastly',
'timeoutMs',
options.timeoutMs ?? DEFAULT_PURGE_TIMEOUT_MS,
);
const doFetch = options.fetch ?? defaultPurgeFetch;

@@ -79,0 +84,0 @@ const headers = { 'Fastly-Key': apiToken };

@@ -76,10 +76,39 @@ // Single responsibility: the HTTP half both remote purge drivers share — one POST with a

const UNSAFE_KEY = /[\s,]/;
const MAX_KEY_LENGTH = 1024;
const MAX_KEY_BYTES = 1024;
const keyProblem = (key: string): string | undefined => {
if (key === '') return 'is empty';
// The CDN's limit is on the HEADER, so it is bytes — and `String.length` is UTF-16 code units,
// which for a non-ASCII tag is a third of the answer: 900 CJK characters measured 900 against a
// 1024 limit and put 2,700 bytes on the wire, where the provider refuses or truncates the key.
// That is the accepted purge that cleared nothing, arriving through the guard meant to stop it.
const keyBytes = (key: string): number => new TextEncoder().encode(key).byteLength;
/**
* The problem AND the repair, together — because there are three ways a key is unpurgeable and only
* one of them is fixed by renaming away a separator. A 900-character CJK tag carries no whitespace
* and no comma, so "rename it so it carries no space or comma" left the next purge failing exactly
* as the last one did: an instruction that does not repair the stated cause is the shape axiom 4
* exists to forbid.
*/
interface KeyProblem {
readonly problem: string;
readonly fix: string;
}
const keyProblem = (key: string): KeyProblem | undefined => {
if (key === '')
return {
problem: 'is empty',
fix: 'give the tag a name in its declareTags(...) call — an empty surrogate key purges nothing',
};
if (UNSAFE_KEY.test(key))
return 'contains whitespace or a comma, which a CDN reads as a separator';
if (key.length > MAX_KEY_LENGTH)
return `is ${key.length} characters, over the 1024-byte key limit`;
return {
problem: 'contains whitespace or a comma, which a CDN reads as a separator',
fix: 'rename the tag in its declareTags(...) call so the key carries no space or comma',
};
const bytes = keyBytes(key);
if (bytes > MAX_KEY_BYTES)
return {
problem: `is ${bytes} bytes, over the ${MAX_KEY_BYTES}-byte key limit`,
fix: `shorten the tag in its declareTags(...) call to at most ${MAX_KEY_BYTES} UTF-8 bytes — the limit is on the header, so it is BYTES, and a non-ASCII character costs two or three of them each`,
};
return undefined;

@@ -98,5 +127,5 @@ };

driver,
detail: `surrogate key ${JSON.stringify(key)} ${problem}`,
detail: `surrogate key ${JSON.stringify(key)} ${problem.problem}`,
retryable: false,
fix: 'rename the tag in its declareTags(...) call so the key carries no space or comma',
fix: problem.fix,
});

@@ -103,0 +132,0 @@ }

@@ -19,3 +19,3 @@ // Tier 2: shared cache over `Bun.redis` (no client dependency — the runtime ships one).

} from './tiers';
import { assertTtl, nowMs } from './tiers';
import { assertFiniteDurationMs, assertTtl, nowMs } from './tiers';

@@ -195,3 +195,7 @@ /** The slice of Bun's Redis client this tier uses. Narrow on purpose: easy to fake in tests. */

const ns = namespaceFor(options.prefix ?? 'x', options.buildId);
const defaultTtlMs = options.defaultTtlMs ?? 300_000;
const defaultTtlMs = assertFiniteDurationMs(
'redis',
'defaultTtlMs',
options.defaultTtlMs ?? 300_000,
);
const clock = options.clock ?? systemClock;

@@ -198,0 +202,0 @@ const jitter: TtlJitter = {

@@ -11,3 +11,9 @@ // The semantic cache for LLM calls: a near-duplicate prompt should not pay for a second

import { tagsIntersect } from './tags';
import { assertTtl, nowMs } from './tiers';
import {
assertFiniteCapacity,
assertFiniteDurationMs,
assertFiniteSimilarityFloor,
assertTtl,
nowMs,
} from './tiers';

@@ -77,5 +83,11 @@ export type Embedding = readonly number[];

export function createMemorySemanticCache(options: SemanticCacheOptions = {}): SemanticCache {
const threshold = options.threshold ?? 0.92;
const maxEntries = options.maxEntries ?? 1000;
const defaultTtlMs = options.defaultTtlMs ?? 3_600_000;
// Screened at construction, both of them: this tier's two knobs are the ones whose failure is
// silent — a floor of NaN matches everything, a ceiling of NaN evicts nothing.
const threshold = assertFiniteSimilarityFloor('semantic', 'threshold', options.threshold ?? 0.92);
const maxEntries = assertFiniteCapacity('semantic', 'maxEntries', options.maxEntries ?? 1000);
const defaultTtlMs = assertFiniteDurationMs(
'semantic',
'defaultTtlMs',
options.defaultTtlMs ?? 3_600_000,
);
const clock = options.clock ?? systemClock;

@@ -98,3 +110,8 @@ const records = new Map<string, SemanticRecord>();

lookup<T>(embedding: Embedding, override?: number): Promise<SemanticHit<T> | undefined> {
const floor = override ?? threshold;
// The override is the same boundary, arriving per call — screened for the same reason, and
// this is the one that can carry a value straight off a request.
const floor =
override === undefined
? threshold
: assertFiniteSimilarityFloor('semantic', 'threshold', override);
let best: SemanticHit<T> | undefined;

@@ -101,0 +118,0 @@ for (const record of live()) {

@@ -8,3 +8,3 @@ // The tier ladder: request-memo -> lru -> redis -> cdn. Reads walk DOWN until a hit, then

import { CACHE_TIERS, systemClock } from '@ultimat3/core';
import { CacheJitterInvalidError, CacheTtlInvalidError } from './errors';
import { CacheJitterInvalidError, CacheLimitInvalidError, CacheTtlInvalidError } from './errors';
import type { CacheFence } from './fence';

@@ -103,2 +103,57 @@ import { markInvalidated, sampleFence } from './fence';

/**
* A tier's ceiling — bytes or entries — screened where it is still nameable. The same rule as
* `assertTtl` beside it, over the other knob an `app.config.ts` carries. A SEPARATE refusal from
* `X_CACHE_TOO_LARGE`, which is one entry against a valid ceiling: this one is the ceiling itself,
* and it needs a screen because a bad ceiling refuses nothing and evicts nothing rather than
* refusing everything. `NaN > x` and `x > NaN` are both false.
*/
export function assertFiniteCapacity(tier: TtlScope, option: string, value: number): number {
if (Number.isSafeInteger(value) && value > 0) return value;
throw new CacheLimitInvalidError({
tier,
option,
value,
expected: 'a whole number greater than zero',
});
}
/**
* A millisecond duration: a driver's request budget (handed to `AbortSignal.timeout`, which THROWS
* on a non-finite one — inside `purgePost`'s try, so the config typo came back as a retryable
* transport failure that never happened) and a tier's own `defaultTtlMs`, which is otherwise only
* screened by `assertTtl` at the first `set`, one write into the process's life.
*
* The `Finite` in all three names here is load-bearing: `bun run finite-bounds` recognises a repair
* by the shape of the CALL, so `assertCapacity` and `assertTimeoutMs` left all nine of this
* package's options reading as unchecked while every one of them was screened.
*/
export function assertFiniteDurationMs(
tier: string,
option: string,
value: number,
/** Passed only where the value is a CALL argument rather than an `app.config.ts` key. */
source?: string | undefined,
): number {
if (Number.isSafeInteger(value) && value > 0) return value;
throw new CacheLimitInvalidError({
tier,
option,
value,
expected: 'a whole number of milliseconds greater than zero',
source,
});
}
/**
* The similarity floor the semantic tier decides "same question" with — a correctness boundary,
* not a tuning knob, so `NaN` here does not loosen it, it removes it: `similarity < NaN` is false
* for every record. Cosine similarity is in [-1, 1] and a floor of 0 already admits an orthogonal
* vector, so anything outside [0, 1] is a number nobody can have meant.
*/
export function assertFiniteSimilarityFloor(tier: TtlScope, option: string, value: number): number {
if (Number.isFinite(value) && value >= 0 && value <= 1) return value;
throw new CacheLimitInvalidError({ tier, option, value, expected: 'a number from 0 to 1' });
}
export function assertTtl(

@@ -209,3 +264,10 @@ key: string,

const flight = createSingleFlight({
deadlineMs: options.loadDeadlineMs ?? DEFAULT_LOAD_DEADLINE_MS,
deadlineMs: assertFiniteDurationMs(
'ladder',
'loadDeadlineMs',
options.loadDeadlineMs ?? DEFAULT_LOAD_DEADLINE_MS,
// Caller-owned: it arrives as a `createCacheStack({ loadDeadlineMs })` argument, so there is
// no `app.config.ts` key to send the reader to.
'the loadDeadlineMs argument to createCacheStack(...)',
),
schedule: options.schedule,

@@ -212,0 +274,0 @@ });