Initial Commit
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { MemoryCache } from "@suaveplan/cache";
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { driveCachePlugin } from "../../src/plugin/plugin.js";
|
||||
|
||||
describe("drive-plugin-cache integration: MemoryCache", () => {
|
||||
let rootDir: string;
|
||||
let drive: Drive;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = mkdtempSync(join(tmpdir(), "drive-plugin-cache-mem-"));
|
||||
const fileStorage = createNodeFsStorage({ rootDir, mkdirp: true });
|
||||
const storage = Object.assign(fileStorage, { id: "default" });
|
||||
drive = new Drive(storage);
|
||||
await drive.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await drive.dispose();
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("second read of the same path is served from the cache", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const cache = new MemoryCache<Uint8Array>({ maxSize: 100 });
|
||||
const handle = await drive.usePlugin(driveCachePlugin({ cache }));
|
||||
const payload = new Uint8Array([0x4d, 0x4f, 0x56, 0x21]);
|
||||
await drive.write("clip.mov", payload);
|
||||
const bytes1 = await drive.read("clip.mov");
|
||||
const bytes2 = await drive.read("clip.mov");
|
||||
expect(Array.from(bytes1)).toEqual(Array.from(payload));
|
||||
expect(Array.from(bytes2)).toEqual(Array.from(payload));
|
||||
const stats = handle.getStats();
|
||||
expect(stats.hits).toBe(1);
|
||||
expect(stats.misses).toBe(1);
|
||||
expect(stats.populations).toBe(1);
|
||||
});
|
||||
|
||||
it("write invalidates a previously populated entry", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const cache = new MemoryCache<Uint8Array>({ maxSize: 100 });
|
||||
const handle = await drive.usePlugin(driveCachePlugin({ cache }));
|
||||
await drive.write("x.bin", new Uint8Array([1, 2, 3]));
|
||||
await drive.read("x.bin"); // populates
|
||||
await drive.write("x.bin", new Uint8Array([9, 9, 9])); // invalidates
|
||||
const fresh = await drive.read("x.bin");
|
||||
expect(Array.from(fresh)).toEqual([9, 9, 9]);
|
||||
// Two writes → two post-success invalidations. The first targets an
|
||||
// unpopulated key (idempotent on the cache); the second actually
|
||||
// evicts the populated entry produced by the intermediate read.
|
||||
expect(handle.getStats().invalidations).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { MemoryCache } from "@suaveplan/cache";
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { driveCachePlugin } from "../../src/plugin/plugin.js";
|
||||
|
||||
describe("drive-plugin-cache integration: shared cache, distinct namespaces", () => {
|
||||
let rootA: string;
|
||||
let rootB: string;
|
||||
let driveA: Drive;
|
||||
let driveB: Drive;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootA = mkdtempSync(join(tmpdir(), "drive-plugin-cache-a-"));
|
||||
rootB = mkdtempSync(join(tmpdir(), "drive-plugin-cache-b-"));
|
||||
const storageA = Object.assign(
|
||||
createNodeFsStorage({ rootDir: rootA, mkdirp: true }),
|
||||
{ id: "default" }
|
||||
);
|
||||
const storageB = Object.assign(
|
||||
createNodeFsStorage({ rootDir: rootB, mkdirp: true }),
|
||||
{ id: "default" }
|
||||
);
|
||||
driveA = new Drive(storageA);
|
||||
driveB = new Drive(storageB);
|
||||
await driveA.initialize();
|
||||
await driveB.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await driveA.dispose();
|
||||
await driveB.dispose();
|
||||
rmSync(rootA, { recursive: true, force: true });
|
||||
rmSync(rootB, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("distinct namespaces isolate entries on a shared cache", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const cache = new MemoryCache<Uint8Array>({ maxSize: 100 });
|
||||
const handleA = await driveA.usePlugin(
|
||||
driveCachePlugin({ cache, namespace: "a" })
|
||||
);
|
||||
const handleB = await driveB.usePlugin(
|
||||
driveCachePlugin({ cache, namespace: "b" })
|
||||
);
|
||||
await driveA.write("clip.mov", new Uint8Array([1]));
|
||||
await driveB.write("clip.mov", new Uint8Array([2]));
|
||||
|
||||
// Populate A's entry under namespace "a"
|
||||
await driveA.read("clip.mov");
|
||||
// B's read for the same path under namespace "b" must miss
|
||||
await driveB.read("clip.mov");
|
||||
|
||||
expect(handleA.getStats().populations).toBe(1);
|
||||
expect(handleB.getStats().populations).toBe(1);
|
||||
expect(handleA.getStats().hits).toBe(0);
|
||||
expect(handleB.getStats().hits).toBe(0);
|
||||
|
||||
// Verify keys are distinct on the shared cache
|
||||
expect(await cache.get("a::clip.mov")).toBeDefined();
|
||||
expect(await cache.get("b::clip.mov")).toBeDefined();
|
||||
expect(Array.from((await cache.get("a::clip.mov")) ?? [])).toEqual([1]);
|
||||
expect(Array.from((await cache.get("b::clip.mov")) ?? [])).toEqual([2]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { IdentityCodec, StorageCache } from "@suaveplan/cache-storage";
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { driveCachePlugin } from "../../src/plugin/plugin.js";
|
||||
|
||||
describe("drive-plugin-cache integration: StorageCache over storage-file", () => {
|
||||
let driveRoot: string;
|
||||
let cacheRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
driveRoot = mkdtempSync(join(tmpdir(), "drive-plugin-cache-drive-"));
|
||||
cacheRoot = mkdtempSync(join(tmpdir(), "drive-plugin-cache-disk-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(driveRoot, { recursive: true, force: true });
|
||||
rmSync(cacheRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("disk-backed cache survives plugin reinstall", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fileStorage = createNodeFsStorage({
|
||||
rootDir: driveRoot,
|
||||
mkdirp: true,
|
||||
});
|
||||
const storage = Object.assign(fileStorage, { id: "default" });
|
||||
const drive = new Drive(storage);
|
||||
await drive.initialize();
|
||||
|
||||
const cacheStorage = createNodeFsStorage({
|
||||
rootDir: cacheRoot,
|
||||
mkdirp: true,
|
||||
});
|
||||
const cache1 = new StorageCache<Uint8Array>({
|
||||
storage: cacheStorage,
|
||||
codec: new IdentityCodec(),
|
||||
});
|
||||
|
||||
const handle1 = await drive.usePlugin(
|
||||
driveCachePlugin({ cache: cache1 })
|
||||
);
|
||||
const payload = new Uint8Array([10, 20, 30, 40]);
|
||||
await drive.write("clip.bin", payload);
|
||||
await drive.read("clip.bin"); // miss + populate
|
||||
expect(handle1.getStats().populations).toBe(1);
|
||||
|
||||
// Build a fresh plugin/cache instance pointing at the same on-disk
|
||||
// cache directory. The previously-populated entry must rehydrate.
|
||||
const cache2 = new StorageCache<Uint8Array>({
|
||||
storage: cacheStorage,
|
||||
codec: new IdentityCodec(),
|
||||
});
|
||||
const fresh = await cache2.get("drive::clip.bin");
|
||||
expect(fresh).toBeDefined();
|
||||
expect(Array.from(fresh ?? new Uint8Array())).toEqual([10, 20, 30, 40]);
|
||||
|
||||
await drive.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { CacheSetOptions, CacheStats } from "@suaveplan/cache";
|
||||
import { AbstractCache } from "@suaveplan/cache";
|
||||
|
||||
/**
|
||||
* In-memory `AbstractCache<Uint8Array>` used by every middleware/handle/plugin
|
||||
* unit test. Tracks every call shape (key, value, signal) so tests can assert
|
||||
* the exact arguments the middleware forwarded into the cache surface.
|
||||
*
|
||||
* This is a real class, not a mock — no `vi.mock`/`jest.mock` involved.
|
||||
*/
|
||||
export class FakeCache extends AbstractCache<Uint8Array> {
|
||||
public store = new Map<string, Uint8Array>();
|
||||
public getCalls: { key: string; signal?: AbortSignal }[] = [];
|
||||
public setCalls: { key: string; value: Uint8Array }[] = [];
|
||||
public deleteCalls: string[] = [];
|
||||
public clearCalls = 0;
|
||||
public throwOnGet: Error | undefined;
|
||||
public throwOnSet: Error | undefined;
|
||||
public throwOnDelete: Error | undefined;
|
||||
|
||||
public async get(
|
||||
key: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<Uint8Array | undefined> {
|
||||
this.getCalls.push({ key, signal });
|
||||
if (this.throwOnGet !== undefined) throw this.throwOnGet;
|
||||
return this.store.get(key);
|
||||
}
|
||||
|
||||
public async set(
|
||||
key: string,
|
||||
value: Uint8Array,
|
||||
_options?: CacheSetOptions
|
||||
): Promise<void> {
|
||||
this.setCalls.push({ key, value });
|
||||
if (this.throwOnSet !== undefined) throw this.throwOnSet;
|
||||
this.store.set(key, value);
|
||||
}
|
||||
|
||||
public async delete(key: string): Promise<boolean> {
|
||||
this.deleteCalls.push(key);
|
||||
if (this.throwOnDelete !== undefined) throw this.throwOnDelete;
|
||||
return this.store.delete(key);
|
||||
}
|
||||
|
||||
public async clear(): Promise<void> {
|
||||
this.clearCalls += 1;
|
||||
this.store.clear();
|
||||
}
|
||||
|
||||
public async has(key: string): Promise<boolean> {
|
||||
return this.store.has(key);
|
||||
}
|
||||
|
||||
public getStats(): CacheStats {
|
||||
return {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
size: this.store.size,
|
||||
hitRate: 0,
|
||||
evictions: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Polyfills for the drive-plugin-cache test environment. Node 18+ provides
|
||||
// `URL`, `AbortController`, `ReadableStream`, and `Response` natively, so no
|
||||
// runtime polyfills are required. File exists so vitest has a stable setup
|
||||
// entry mirroring sibling drive packages.
|
||||
export {};
|
||||
Reference in New Issue
Block a user