diff --git a/diigo-cache/src/cache.ts b/diigo-cache/src/cache.ts index a47dd4b..a4a9996 100644 --- a/diigo-cache/src/cache.ts +++ b/diigo-cache/src/cache.ts @@ -285,8 +285,15 @@ abstract class PagedCache extends DurableObject { if (!init) throw new Error("not wired: this cache id was never set up"); this.ctx.storage.sql.exec(SCHEMA_V1); - // one diigo call path at a time, min spacing MS_PER_CALL (webapp's - // Gate pattern; per activation) + const hit = await this.#pageFromStorage(pageNum, count); + if (hit) { + console.log({ event: "cache", kind: this.kind, page: pageNum, outcome: "hit", items: hit.items.length }); + return hit; + } + + // a miss calls diigo: one call path at a time, min spacing + // MS_PER_CALL (webapp's Gate pattern; per activation). Hits above + // never wait. let before; do { before = this.#step; @@ -294,11 +301,11 @@ abstract class PagedCache extends DurableObject { } while (before !== this.#step); this.#step += 1; this.#pause = this.#clock.sleep(MS_PER_CALL); - - const hit = await this.#pageFromStorage(pageNum, count); - if (hit) { - console.log({ event: "cache", kind: this.kind, page: pageNum, outcome: "hit", items: hit.items.length }); - return hit; + // a request ahead of us at the gate may have etched this page + const late = await this.#pageFromStorage(pageNum, count); + if (late) { + console.log({ event: "cache", kind: this.kind, page: pageNum, outcome: "hit", items: late.items.length }); + return late; } // RPC stubs (the app and each pipelined step) are disposed once the diff --git a/diigo-cache/tests/caches.test.ts b/diigo-cache/tests/caches.test.ts index 4559ceb..31bdc7d 100644 --- a/diigo-cache/tests/caches.test.ts +++ b/diigo-cache/tests/caches.test.ts @@ -12,6 +12,7 @@ const WEBAPP_ID = "fake-webapp-id"; type TestEnv = { CACHE_DO: DurableObjectNamespace; PUBLIC_DO: DurableObjectNamespace; + CLOCKED_DO: DurableObjectNamespace; WEBAPP: { calls(): Promise }; }; const testEnv = env as unknown as TestEnv; @@ -109,6 +110,18 @@ describe("page size", () => { }); }); +describe("pacing", () => { + it("paces diigo calls only: a cache hit doesn't wait", async () => { + const stub = testEnv.CLOCKED_DO.get(testEnv.CLOCKED_DO.newUniqueId()); + await stub.setup({ webappId: WEBAPP_ID }); + await stub.page(1, PAGE_SIZE); + const afterMiss = (await stub.sleeps()).length; + expect(afterMiss).toBeGreaterThan(0); + for (let i = 0; i < 3; i += 1) expect((await stub.page(1, PAGE_SIZE)).cached).toBe(true); + expect(await stub.sleeps()).toHaveLength(afterMiss); + }); +}); + describe("eviction", () => { it("a cache miss leaves no undisposed RPC stubs to block eviction", async () => { const stub = testEnv.CACHE_DO.get(testEnv.CACHE_DO.newUniqueId()); diff --git a/diigo-cache/tests/test-worker.ts b/diigo-cache/tests/test-worker.ts index eb68698..457beb2 100644 --- a/diigo-cache/tests/test-worker.ts +++ b/diigo-cache/tests/test-worker.ts @@ -12,8 +12,23 @@ export { default } from "../src/index.js"; /** 2026-10-07T12:00:00Z */ export const FIXED_NOW = Date.UTC(2026, 9, 7, 12, 0, 0); +/** a fixed clock that records each sleep it is asked for */ export class FixedClockCache extends DiigoCacheLogic { + #sleeps: number[] = []; + constructor(ctx: DurableObjectState, env: CacheEnv) { - super(ctx, env, { now: () => FIXED_NOW, sleep: async () => {} }); + const sleeps: number[] = []; + super(ctx, env, { + now: () => FIXED_NOW, + sleep: async (ms) => { + sleeps.push(ms); + }, + }); + this.#sleeps = sleeps; + } + + /** the sleeps so far (test hook) */ + async sleeps(): Promise { + return [...this.#sleeps]; } }