Initial Commit
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { MemoryCache } from "@suaveplan/cache";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { cachedStorage } from "../../src/factory/factory.js";
|
||||
import { FakeStorage } from "../setup/fake-storage.js";
|
||||
|
||||
/**
|
||||
* Integration: a long-lived cache instance survives the
|
||||
* `CachedStorage` wrapper being dropped and re-instantiated. We
|
||||
* stand-in for a file-backed cache with a shared `MemoryCache`
|
||||
* instance: the cache outlives the wrapper, and the second wrapper
|
||||
* (constructed after the first is dropped) observes a hit on the first
|
||||
* read — same behaviour any persistent backend (file, OPFS, redis)
|
||||
* would exhibit.
|
||||
*/
|
||||
describe("integration: cache survives wrapper re-instantiation", () => {
|
||||
it("populates, drops the wrapper, and the next wrapper observes a hit", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const inner = new FakeStorage();
|
||||
const sharedCache = new MemoryCache<Uint8Array>();
|
||||
inner.seed("v.bin", new Uint8Array([9, 9, 9]));
|
||||
|
||||
// First wrapper populates the shared cache and is then dropped.
|
||||
const first = cachedStorage(inner, {
|
||||
cache: sharedCache,
|
||||
namespace: "shared",
|
||||
});
|
||||
await first.get("v.bin");
|
||||
expect(first.getCacheStats().populations).toBe(1);
|
||||
|
||||
// Drop the reference. The shared cache persists.
|
||||
// (No explicit teardown needed — `MemoryCache` has no resource handle.)
|
||||
|
||||
// New wrapper over the same cache + same inner. Inner.getCalls is now 1.
|
||||
// Seed the inner with a different value to prove the cached bytes
|
||||
// win on the next read.
|
||||
inner.seed("v.bin", new Uint8Array([1, 2, 3]));
|
||||
const second = cachedStorage(inner, {
|
||||
cache: sharedCache,
|
||||
namespace: "shared",
|
||||
});
|
||||
const out = await second.get("v.bin");
|
||||
expect(out).toEqual(new Uint8Array([9, 9, 9])); // served from cache
|
||||
expect(inner.getCalls.length).toBe(1); // unchanged after the second read
|
||||
expect(second.getCacheStats().hits).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { MemoryCache } from "@suaveplan/cache";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { cachedStorage } from "../../src/factory/factory.js";
|
||||
import { FakeStorage } from "../setup/fake-storage.js";
|
||||
|
||||
/**
|
||||
* Integration: real `MemoryCache` from `@suaveplan/cache` (no fake, no
|
||||
* stub) sitting in front of an `AbstractStorage` instance that
|
||||
* represents a remote backend (`FakeStorage` here, which is a real
|
||||
* `AbstractStorage` subclass over an in-memory `Map`). Two reads of the
|
||||
* same key MUST hit the inner exactly once; the second read MUST be
|
||||
* served from the cache.
|
||||
*/
|
||||
describe("integration: MemoryCache in front of a remote storage", () => {
|
||||
it("two reads of the same key hit inner once", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const inner = new FakeStorage();
|
||||
const cache = new MemoryCache<Uint8Array>();
|
||||
const wrapper = cachedStorage(inner, { cache, namespace: "remote" });
|
||||
inner.seed("clip.mov", new Uint8Array([1, 2, 3, 4]));
|
||||
const first = await wrapper.get("clip.mov");
|
||||
const second = await wrapper.get("clip.mov");
|
||||
expect(first).toEqual(new Uint8Array([1, 2, 3, 4]));
|
||||
expect(second).toEqual(new Uint8Array([1, 2, 3, 4]));
|
||||
expect(inner.getCalls.length).toBe(1);
|
||||
expect(wrapper.getCacheStats()).toMatchObject({
|
||||
hits: 1,
|
||||
misses: 1,
|
||||
populations: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("write invalidates so the next read refetches", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const inner = new FakeStorage();
|
||||
const cache = new MemoryCache<Uint8Array>();
|
||||
const wrapper = cachedStorage(inner, { cache, namespace: "remote" });
|
||||
inner.seed("clip.mov", new Uint8Array([1]));
|
||||
await wrapper.get("clip.mov"); // populates
|
||||
await wrapper.set("clip.mov", new Uint8Array([2, 2])); // invalidates
|
||||
const refreshed = await wrapper.get("clip.mov"); // miss again
|
||||
expect(refreshed).toEqual(new Uint8Array([2, 2]));
|
||||
expect(inner.getCalls.length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { cachedStorage } from "../../src/factory/factory.js";
|
||||
import { KeysCapableFakeCache } from "../setup/fake-cache.js";
|
||||
import { FakeStorage } from "../setup/fake-storage.js";
|
||||
|
||||
/**
|
||||
* Integration: two `CachedStorage` wrappers sharing one
|
||||
* `AbstractCache` instance with distinct namespaces. Neither wrapper
|
||||
* observes the other's entries on `get`, and `wrapperA.clear()` MUST
|
||||
* NOT evict `wrapperB`'s entries.
|
||||
*
|
||||
* Uses the `KeysCapableFakeCache` test helper because the canonical
|
||||
* `MemoryCache` from `@suaveplan/cache` does not expose `keys()`. The
|
||||
* test helper is a real `AbstractCache<Uint8Array>` subclass — not a
|
||||
* mock — so the integration exercises real iteration over real
|
||||
* cache entries.
|
||||
*/
|
||||
describe("integration: namespaced isolation on a shared cache", () => {
|
||||
it("distinct namespaces isolate entries on a shared cache", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const innerA = new FakeStorage();
|
||||
const innerB = new FakeStorage();
|
||||
const shared = new KeysCapableFakeCache();
|
||||
|
||||
const wrapperA = cachedStorage(innerA, {
|
||||
cache: shared,
|
||||
namespace: "a",
|
||||
});
|
||||
const wrapperB = cachedStorage(innerB, {
|
||||
cache: shared,
|
||||
namespace: "b",
|
||||
});
|
||||
|
||||
innerA.seed("clip.mov", new Uint8Array([0xa]));
|
||||
innerB.seed("clip.mov", new Uint8Array([0xb]));
|
||||
|
||||
// Wrapper A populates its entry under "a::clip.mov".
|
||||
await wrapperA.get("clip.mov");
|
||||
expect(shared._peek("a::clip.mov")).toEqual(new Uint8Array([0xa]));
|
||||
expect(shared._peek("b::clip.mov")).toBeUndefined();
|
||||
|
||||
// Wrapper B observes a miss for the same logical key (different namespace).
|
||||
const fromB = await wrapperB.get("clip.mov");
|
||||
expect(fromB).toEqual(new Uint8Array([0xb]));
|
||||
expect(wrapperB.getCacheStats().misses).toBe(1);
|
||||
expect(shared._peek("b::clip.mov")).toEqual(new Uint8Array([0xb]));
|
||||
});
|
||||
|
||||
it("clear on one wrapper does not evict the other's entries", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const innerA = new FakeStorage();
|
||||
const innerB = new FakeStorage();
|
||||
const shared = new KeysCapableFakeCache();
|
||||
const wrapperA = cachedStorage(innerA, {
|
||||
cache: shared,
|
||||
namespace: "a",
|
||||
});
|
||||
const wrapperB = cachedStorage(innerB, {
|
||||
cache: shared,
|
||||
namespace: "b",
|
||||
});
|
||||
|
||||
// Populate both.
|
||||
innerA.seed("x", new Uint8Array([0xa, 1]));
|
||||
innerA.seed("y", new Uint8Array([0xa, 2]));
|
||||
innerB.seed("z", new Uint8Array([0xb, 1]));
|
||||
await wrapperA.get("x");
|
||||
await wrapperA.get("y");
|
||||
await wrapperB.get("z");
|
||||
|
||||
expect(shared._peek("a::x")).toBeDefined();
|
||||
expect(shared._peek("a::y")).toBeDefined();
|
||||
expect(shared._peek("b::z")).toBeDefined();
|
||||
|
||||
// Wrapper A clears.
|
||||
await wrapperA.clear();
|
||||
|
||||
expect(shared._peek("a::x")).toBeUndefined();
|
||||
expect(shared._peek("a::y")).toBeUndefined();
|
||||
expect(shared._peek("b::z")).toEqual(new Uint8Array([0xb, 1]));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user