New:Socket for Asana Is Now Available.Learn more
Sign In

@ultimat3/cache

Package Overview
Dependencies
Maintainers
1
Versions
23
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
7.0.0
to
8.0.0
+11
-4
CLAUDE.md

@@ -43,5 +43,6 @@ # @ultimat3/cache — agent notes

`TierName` is a position on the ladder.
- **A refusal is rendered with `renderThrowable()`, never `error.message`** — the four sites that
absorb one (`bestEffort`'s log entry, and `fanOut`'s tier, ISR and broadcast catch blocks). A
tier, a revalidator and a broadcast are all app-supplied, so the value they reject with is too:
- **A refusal is rendered with `renderThrowable()`, never `error.message`** — the five sites that
absorb one (`bestEffort`'s log entry, `fanOut`'s tier, ISR and broadcast catch blocks, and
`purgePost`'s transport catch). A tier, a revalidator, a broadcast and the `fetch` a purge driver
is given are all app-supplied, so the value they reject with is too:
`instanceof` runs a `Proxy`'s `getPrototypeOf` trap and `String()` runs `Symbol.toPrimitive`, so

@@ -135,3 +136,9 @@ building the log line used to raise INSTEAD of absorbing the refusal — on the business write that

stale to that caller. `work` reads the merge through `shared()` **after** the load, or it sees
only what the leader brought.
only what the leader brought — and **once more after the fill**, because the flight stays open
for the whole ladder and a joiner merging a tag mid-fill hit the identical hole one rung later.
The second read re-fills EVERY tier rather than the rungs still to come: re-reading per tier
would land the near tier — the one every later read hits first — with the FEWEST tags, so an
invalidation would clear the far rungs and leave the near one serving. `tagsAddedSince` in
`set-options.ts` is what makes the second pass conditional; `tiers.test.ts`'s
`a single-flight joiner that arrives during the FILL` is what notices.
- **`negativeTtlMs` is the stack's decision, not a tier's.** Only `createCacheStack` sees what

@@ -138,0 +145,0 @@ `load()` answered, so the `null`/`undefined` branch lives in `ttlOptionsFor` there and reaches a

{
"name": "@ultimat3/cache",
"version": "7.0.0",
"version": "8.0.0",
"description": "Tagged caching: request memo, LRU, Redis, CDN — one invalidation graph",

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

"dependencies": {
"@ultimat3/core": "7.0.0"
"@ultimat3/core": "8.0.0"
}
}

@@ -6,2 +6,3 @@ // Single responsibility: the HTTP half both remote purge drivers share — one POST with a

import { renderThrowable } from '@ultimat3/core';
import { CacheDriverUnavailableError, CachePurgeFailedError } from './errors';

@@ -156,3 +157,7 @@

} catch (error) {
const reason = error instanceof Error ? error.message : 'the request failed before a response';
// `renderThrowable`, never `error.message` behind an `instanceof`: `fetch` is INJECTED here,
// so the rejection is whatever a driver or a test double threw — and `instanceof` itself
// throws on a `Proxy` whose `getPrototypeOf` does, which would replace the coded refusal this
// catch exists to raise with a bare `TypeError` from inside it. Same rule as `invalidate.ts`.
const reason = renderThrowable(error);
throw new CachePurgeFailedError({

@@ -159,0 +164,0 @@ driver: input.driver,

@@ -66,1 +66,18 @@ // How two callers' `CacheSetOptions` become one write, and how a `null` load picks its TTL. Both

}
/**
* Did `latest` gain a tag `written` does not carry?
*
* Compared on the wire form — the identity every tier indexes by and the one `mergeTags` above
* dedupes on — so "already written" means the same thing to both, and a re-fill is asked for
* exactly when a joiner brought something new.
*/
export function tagsAddedSince(
written: CacheSetOptions | undefined,
latest: CacheSetOptions | undefined,
): boolean {
const added = latest?.tags;
if (added === undefined || added.length === 0) return false;
const seen = new Set((written?.tags ?? []).map(serializeTag));
return added.some((owned) => !seen.has(serializeTag(owned)));
}

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

import { markInvalidated, sampleFence } from './fence';
import { mergeSetOptions, ttlOptionsFor } from './set-options';
import { mergeSetOptions, tagsAddedSince, ttlOptionsFor } from './set-options';
import { createSingleFlight } from './single-flight';

@@ -259,2 +259,6 @@ import type { CacheTag } from './tags';

async read<T>(key: string, load: () => Promise<T>, setOptions?: CacheSetOptions): Promise<T> {
// Outside the flight on purpose, and the cost is known: N concurrent misses each walk the
// ladder before any of them joins, so a cold key pays N gets per rung. Moving it inside
// would serialise every HIT behind whichever caller happened to arrive first — the common
// case paying for the rare one. Carried as a Low; measure before changing it.
const hit = await lookup<T>(key, setOptions);

@@ -278,5 +282,18 @@ if (hit !== undefined) return hit.value;

// a tag that arrived mid-load is fenced back to the sample rather than from now.
const publish = async (options: CacheSetOptions | undefined): Promise<void> => {
if (options?.tags !== undefined) fence.cover({ tags: options.tags });
await fill(key, value, options, fence);
};
const merged = shared() ?? setOptions;
if (merged?.tags !== undefined) fence.cover({ tags: merged.tags });
await fill(key, value, merged, fence);
await publish(merged);
// The flight stays open until this whole `work` settles, and `fill` is one await per
// rung — so a joiner can still merge a tag after the read above, and the entry that
// landed would carry the leader's tags alone, which `invalidateTags` can never reach.
// Re-read once and re-fill EVERY tier: re-reading per tier instead would land the near
// tier — the one every later read hits first — with the FEWEST tags, so an invalidation
// would clear the far rungs and leave the near one serving. A joiner arriving inside
// the second pass is left where a plain cache hit already leaves one: reading a value
// that was published without its tag.
const late = shared() ?? setOptions;
if (tagsAddedSince(merged, late)) await publish(late);
return value;

@@ -283,0 +300,0 @@ },