Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
# UriLruCache
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`cache.ts` declares the optional in-memory LRU used by the storage shim
when a caller passes `options.cache = { maxBytes }`. The cache is keyed
by the full URI string, holds raw `Uint8Array` bodies, and evicts oldest
entries first until the cumulative byte total is at or below the configured
ceiling. The implementation is intentionally minimal — just enough surface
for the `get` / `set` / `has` triad the shim needs.
The shim defaults to OFF. When the LRU is enabled, every successful `get`
result is inserted; subsequent `get`s of the same URI return the cached
bytes without invoking the injected `FetchLike`. Writes are not relevant
(bundles are immutable, every write throws `READ_ONLY` upstream of the
cache).
## Features
- O(1) `get`/`set`/`has` via the underlying `Map`.
- LRU recency refresh on `get` (delete + re-insert at tail).
- Byte-bounded eviction policy — oldest-first until
`currentBytes <= maxBytes`.
- Single-entry oversize is rejected silently (the entry is not inserted
and no eviction loop fires) so a runaway 500 MB payload cannot flush
the entire cache.
- `bytes` / `size` accessors for diagnostic and test inspection.
## Usage: Basic
```ts
import { UriLruCache } from "@suaveplan/storage-uri-plugin-bundle";
const cache = new UriLruCache(1024);
cache.set("bundle://x/a", new Uint8Array(512));
cache.set("bundle://x/b", new Uint8Array(512));
cache.get("bundle://x/a"); // → Uint8Array (and refresh recency)
cache.set("bundle://x/c", new Uint8Array(512));
cache.has("bundle://x/b"); // → false (evicted as oldest)
```
## Usage: Advanced
```ts
import { UriLruCache } from "@suaveplan/storage-uri-plugin-bundle";
const cache = new UriLruCache(100);
cache.set("k1", new Uint8Array(50));
cache.set("k2", new Uint8Array(50));
cache.set("k3", new Uint8Array(50));
console.log(cache.size, cache.bytes); // → 2 100
console.log(cache.has("k1")); // → false (oldest evicted)
```
## API Reference
- `new UriLruCache(maxBytes)` — construct an empty cache with the
byte ceiling.
- `get(key): Uint8Array | undefined` — refresh recency and return.
- `set(key, value): void` — insert and evict to satisfy the ceiling.
- `has(key): boolean` — non-mutating presence check.
- `size: number` — entry count.
- `bytes: number` — cumulative byte total.
## Implementation Notes
The class delegates everything to a plain `Map`. JavaScript `Map`
preserves insertion order, so `map.keys().next().value` returns the
oldest key — exactly the LRU candidate. The recency refresh on `get`
is a delete + re-insert; the cost is O(1). Cached buffers are shared
references — callers must NOT mutate a returned `Uint8Array` in place
or future cache hits will surface the mutation. The shim guarantees
immutability by treating cache results as read-only, which is the same
invariant the entire `bundle://` contract enforces upstream.

View File

@@ -0,0 +1,167 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { bundleUriPlugin } from "../factory/factory.js";
import {
getInstruments,
resetInstruments,
setBundleUriMeterProvider,
} from "../otel/otel.js";
import { UriLruCache } from "./cache.js";
describe("UriLruCache (unit)", () => {
it("returns undefined for missing keys", { timeout: 5000 }, () => {
const cache = new UriLruCache(1024);
expect(cache.get("missing")).toBeUndefined();
expect(cache.has("missing")).toBe(false);
expect(cache.size).toBe(0);
expect(cache.bytes).toBe(0);
});
it("stores and retrieves bytes", { timeout: 5000 }, () => {
const cache = new UriLruCache(1024);
const v = new Uint8Array([1, 2, 3]);
cache.set("k", v);
expect(cache.get("k")).toBe(v);
expect(cache.bytes).toBe(3);
expect(cache.size).toBe(1);
});
it("refreshes recency on get (delete-then-reinsert)", {
timeout: 5000,
}, () => {
const cache = new UriLruCache(100);
cache.set("a", new Uint8Array(30));
cache.set("b", new Uint8Array(30));
// a is older, b is newer; reading a makes it newest.
cache.get("a");
cache.set("c", new Uint8Array(50));
// Total = 30+30+50 = 110 > 100, so oldest (b) evicted.
expect(cache.has("a")).toBe(true);
expect(cache.has("b")).toBe(false);
expect(cache.has("c")).toBe(true);
});
it("LRU evicts oldest when maxBytes exceeded", { timeout: 5000 }, () => {
const cache = new UriLruCache(100);
cache.set("a", new Uint8Array(50));
cache.set("b", new Uint8Array(50));
cache.set("c", new Uint8Array(50));
expect(cache.has("a")).toBe(false);
expect(cache.has("b")).toBe(true);
expect(cache.has("c")).toBe(true);
});
it("rejects single-entry oversize without evicting world", {
timeout: 5000,
}, () => {
const cache = new UriLruCache(100);
cache.set("small", new Uint8Array(50));
cache.set("huge", new Uint8Array(500));
expect(cache.has("small")).toBe(true);
expect(cache.has("huge")).toBe(false);
});
it("re-inserting same key updates bytes correctly", {
timeout: 5000,
}, () => {
const cache = new UriLruCache(100);
cache.set("a", new Uint8Array(40));
cache.set("a", new Uint8Array(60));
expect(cache.bytes).toBe(60);
expect(cache.size).toBe(1);
});
});
describe("cache integration with shim", () => {
it("cache off by default does not hit", {
timeout: 30000,
}, async () => {
let invoked = 0;
const fetchFn: typeof fetch = async () =>
new Response(new Uint8Array([1, 2, 3]), { status: 200 });
const plugin = bundleUriPlugin({
packageSlug: "x",
rootUrl: "https://example/x/",
fetch: () => {
invoked++;
return fetchFn("x");
},
});
await plugin.adapter.get("k");
await plugin.adapter.get("k");
expect(invoked).toBe(2);
});
it("cache hit returns cached bytes without fetch", {
timeout: 30000,
}, async () => {
const hits: Array<Record<string, unknown> | undefined> = [];
setBundleUriMeterProvider({
getMeter: () => ({
createCounter: (name: string) => ({
add: (_: number, attrs?: Record<string, unknown>) => {
if (name === "bundle_uri.cache_hits_total") {
hits.push(attrs);
}
},
}),
createHistogram: () => ({ record: () => undefined }),
}),
} as unknown as Parameters<typeof setBundleUriMeterProvider>[0]);
resetInstruments();
getInstruments(true);
let invoked = 0;
const fetchFn: typeof fetch = async () => {
invoked++;
return new Response(new Uint8Array([7, 8, 9]), { status: 200 });
};
const plugin = bundleUriPlugin({
packageSlug: "x",
rootUrl: "https://example/x/",
fetch: fetchFn,
cache: { maxBytes: 1024 },
});
const a = await plugin.adapter.get("k");
const b = await plugin.adapter.get("k");
expect(invoked).toBe(1);
expect(Array.from(a ?? [])).toEqual([7, 8, 9]);
expect(Array.from(b ?? [])).toEqual([7, 8, 9]);
expect(hits.length).toBeGreaterThanOrEqual(1);
expect(hits[0]?.["root_kind"]).toBe("http");
setBundleUriMeterProvider(undefined);
resetInstruments();
});
it("LRU evicts older entries when maxBytes exceeded", {
timeout: 30000,
}, async () => {
let invoked = 0;
const responses = new Map<string, Uint8Array>([
["k1", new Uint8Array(50)],
["k2", new Uint8Array(50)],
["k3", new Uint8Array(50)],
]);
const fetchFn: typeof fetch = async (input) => {
invoked++;
const u = input instanceof URL ? input : new URL(String(input));
const key = u.pathname.slice(1);
const body = responses.get(key);
return new Response(body, { status: 200 });
};
const plugin = bundleUriPlugin({
packageSlug: "x",
rootUrl: "https://example/",
fetch: fetchFn,
cache: { maxBytes: 100 },
});
await plugin.adapter.get("k1");
await plugin.adapter.get("k2");
await plugin.adapter.get("k3"); // evicts k1
expect(invoked).toBe(3);
await plugin.adapter.get("k1"); // miss again
expect(invoked).toBe(4);
await plugin.adapter.get("k3"); // hit
expect(invoked).toBe(4);
});
});

View File

@@ -0,0 +1,61 @@
/**
* In-memory LRU cache keyed by full URI. Off by default; instantiated
* by the storage shim when `options.cache.maxBytes` is supplied.
*
* The implementation is a plain `Map`-backed LRU: insertion order is
* the LRU order, `get` deletes-and-reinserts to refresh recency, and
* `set` evicts oldest entries until `totalBytes <= maxBytes`. Single-
* entry oversize is rejected (the entry is not inserted, no eviction
* loop runs forever).
*/
export class UriLruCache {
private readonly map = new Map<string, Uint8Array>();
private currentBytes = 0;
public constructor(private readonly maxBytes: number) {}
public get(key: string): Uint8Array | undefined {
const value = this.map.get(key);
if (value === undefined) return undefined;
// Refresh recency: re-insert at the tail.
this.map.delete(key);
this.map.set(key, value);
return value;
}
public set(key: string, value: Uint8Array): void {
const size = value.byteLength;
// Single-entry oversize — do not insert, do not evict the world.
if (size > this.maxBytes) return;
const prev = this.map.get(key);
if (prev !== undefined) {
this.currentBytes -= prev.byteLength;
this.map.delete(key);
}
this.map.set(key, value);
this.currentBytes += size;
while (this.currentBytes > this.maxBytes) {
// Invariant: currentBytes > 0 implies map.size > 0, so the
// entries iterator yields a defined entry. Cast is therefore
// safe and matches the runtime invariant.
const [oldestKey, oldestVal] = this.map.entries().next().value as [
string,
Uint8Array,
];
this.map.delete(oldestKey);
this.currentBytes -= oldestVal.byteLength;
}
}
public has(key: string): boolean {
return this.map.has(key);
}
public get size(): number {
return this.map.size;
}
public get bytes(): number {
return this.currentBytes;
}
}

View File

@@ -0,0 +1 @@
export { UriLruCache } from "./cache.js";