import { describe, expect, it, vi } from "vitest";
import { createBobbinClient, type BobbinContext } from "#lib/api/client.ts";
import { IdentityCache, type MiniDoc } from "#lib/api/identity.ts";
import type { Fetch } from "#lib/fetch.ts";
const DOC: MiniDoc = {
did: "did:plc:x",
handle: "alice.test",
pds: "https://pds.example",
signing_key: "zQ3shSiLsnqpyQ4SfDTT1D8qzFEoeYT8rSDXW6o8pVY7VcRBJ"
};
const docResponse = (doc: MiniDoc): Response =>
new Response(JSON.stringify(doc), {
status: 200,
headers: { "content-type": "application/json" }
});
const makeCtx = (fetchMock: Fetch): BobbinContext =>
createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
const deferred = <T>(): { promise: Promise<T>; resolve: (value: T) => void } => {
let resolve!: (value: T) => void;
const promise = new Promise<T>((r) => {
resolve = r;
});
return { promise, resolve };
};
describe("IdentityCache.resolve", () => {
it("fetches on the first miss then serves the cached document", async () => {
const fetchMock = vi.fn<Fetch>().mockResolvedValue(docResponse(DOC));
const cache = new IdentityCache(makeCtx(fetchMock));
const first = await cache.resolve("alice.test");
const second = await cache.resolve("alice.test");
expect(first).toEqual(DOC);
expect(second).toEqual(DOC);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect((fetchMock.mock.calls[0][0] as URL).searchParams.get("identifier")).toBe(
"alice.test"
);
});
it("coalesces concurrent misses for the same key into one in-flight fetch", async () => {
const gate = deferred<Response>();
const fetchMock = vi.fn<Fetch>().mockReturnValue(gate.promise);
const cache = new IdentityCache(makeCtx(fetchMock));
const a = cache.resolve("alice.test");
const b = cache.resolve("alice.test");
expect(fetchMock).toHaveBeenCalledTimes(1);
gate.resolve(docResponse(DOC));
const [ra, rb] = await Promise.all([a, b]);
expect(ra).toEqual(DOC);
expect(rb).toEqual(DOC);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("re-fetches after the in-flight promise settles (no permanent stampede lock)", async () => {
const fetchMock = vi.fn<Fetch>().mockResolvedValue(docResponse(DOC));
const cache = new IdentityCache(makeCtx(fetchMock));
await cache.resolve("bob.test");
fetchMock.mockResolvedValueOnce(
docResponse({
did: "did:plc:y",
handle: "charlie.test",
pds: "https://pds.example",
signing_key: "zQ3shSiLsnqpyQ4SfDTT1D8qzFEoeYT8rSDXW6o8pVY7VcRBJ"
})
);
await cache.resolve("charlie.test");
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it("populates both directions of the index from a resolved document", async () => {
const fetchMock = vi.fn<Fetch>().mockResolvedValue(docResponse(DOC));
const cache = new IdentityCache(makeCtx(fetchMock));
await cache.resolve("alice.test");
expect(cache.didFor("alice.test")).toBe("did:plc:x");
expect(cache.handleFor("did:plc:x")).toBe("alice.test");
});
});
describe("IdentityCache.prime & map selection", () => {
it("seeds both directions without any fetch", () => {
const fetchMock = vi.fn<Fetch>();
const cache = new IdentityCache(makeCtx(fetchMock));
cache.prime(DOC);
expect(cache.didFor("alice.test")).toBe("did:plc:x");
expect(cache.handleFor("did:plc:x")).toBe("alice.test");
expect(fetchMock).not.toHaveBeenCalled();
});
it("resolves a DID against the did map and a handle against the handle map", async () => {
const fetchMock = vi.fn<Fetch>();
const cache = new IdentityCache(makeCtx(fetchMock));
cache.prime(DOC);
await expect(cache.resolve("did:plc:x")).resolves.toEqual(DOC);
await expect(cache.resolve("alice.test")).resolves.toEqual(DOC);
expect(fetchMock).not.toHaveBeenCalled();
// unknown dids miss the handle map and fetch.
fetchMock.mockResolvedValueOnce(
docResponse({
did: "did:plc:z",
handle: "dan.test",
pds: "https://pds.example",
signing_key: "zQ3shSiLsnqpyQ4SfDTT1D8qzFEoeYT8rSDXW6o8pVY7VcRBJ"
})
);
await cache.resolve("did:plc:z");
expect(fetchMock).toHaveBeenCalledTimes(1);
expect((fetchMock.mock.calls[0][0] as URL).searchParams.get("identifier")).toBe(
"did:plc:z"
);
});
});