Skip to content

Commit 0aedd1b

Browse files
fix(appkit): honor TTL in CacheManager.getOrExecute (#326)
Signed-off-by: Tim Hoare <t.hoare@codat.io> Signed-off-by: TimHoare <t.hoare@codat.io> Co-authored-by: Mario Cadenas <17888484+MarioCadenas@users.noreply.github.com>
1 parent 0803942 commit 0aedd1b

2 files changed

Lines changed: 42 additions & 13 deletions

File tree

packages/appkit/src/cache/index.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createHash } from "node:crypto";
22
import { ApiError, WorkspaceClient } from "@databricks/sdk-experimental";
3-
import type { CacheConfig, CacheStorage } from "shared";
3+
import type { CacheConfig, CacheEntry, CacheStorage } from "shared";
44
import { createLakebasePool } from "../connectors/lakebase";
55
import { AppKitError, ExecutionError, InitializationError } from "../errors";
66
import { createLogger } from "../logging/logger";
@@ -202,8 +202,7 @@ export class CacheManager {
202202
},
203203
async (span) => {
204204
try {
205-
// check if the value is in the cache
206-
const cached = await this.storage.get<T>(cacheKey);
205+
const cached = await this.getValid<T>(cacheKey);
207206
if (cached !== null) {
208207
span.setAttribute("cache.hit", true);
209208
span.setStatus({ code: SpanStatusCode.OK });
@@ -216,7 +215,7 @@ export class CacheManager {
216215
cache_key: cacheKey,
217216
});
218217

219-
return cached.value as T;
218+
return cached.value;
220219
}
221220

222221
// check if the value is being processed by another request
@@ -308,14 +307,26 @@ export class CacheManager {
308307
// probabilistic cleanup trigger
309308
this.maybeCleanup();
310309

310+
const entry = await this.getValid<T>(key);
311+
return entry?.value ?? null;
312+
}
313+
314+
/**
315+
* Get a cached entry only if it has not expired.
316+
* Returns null on miss or expired (and deletes the expired entry).
317+
*
318+
* Storage implementations return entries unconditionally — expiry handling
319+
* lives at the CacheManager layer.
320+
*/
321+
private async getValid<T>(key: string): Promise<CacheEntry<T> | null> {
311322
const entry = await this.storage.get<T>(key);
312323
if (!entry) return null;
313324

314325
if (Date.now() > entry.expiry) {
315326
await this.storage.delete(key);
316327
return null;
317328
}
318-
return entry.value as T;
329+
return entry;
319330
}
320331

321332
/** Probabilistically trigger cleanup of expired entries (fire-and-forget) */
@@ -386,14 +397,8 @@ export class CacheManager {
386397
async has(key: string): Promise<boolean> {
387398
if (!this.config.enabled) return false;
388399

389-
const entry = await this.storage.get(key);
390-
if (!entry) return false;
391-
392-
if (Date.now() > entry.expiry) {
393-
await this.storage.delete(key);
394-
return false;
395-
}
396-
return true;
400+
const entry = await this.getValid(key);
401+
return entry !== null;
397402
}
398403

399404
/**

packages/appkit/src/cache/tests/cache-manager.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,30 @@ describe("CacheManager", () => {
354354
expect(result1).toBe("user1-data");
355355
expect(result2).toBe("user2-data");
356356
});
357+
358+
test("should re-execute function when cached entry has expired", async () => {
359+
const cache = await CacheManager.getInstance({
360+
storage: createMockStorage(),
361+
});
362+
let calls = 0;
363+
const fn = vi.fn().mockImplementation(async () => `result-${++calls}`);
364+
365+
// First call - populates cache with a 1ms TTL
366+
const r1 = await cache.getOrExecute(["key"], fn, "user1", {
367+
ttl: 0.001,
368+
});
369+
expect(r1).toBe("result-1");
370+
371+
// Wait for expiry
372+
await new Promise((resolve) => setTimeout(resolve, 10));
373+
374+
// Second call - cached entry is past its expiry, fn must run again
375+
const r2 = await cache.getOrExecute(["key"], fn, "user1", {
376+
ttl: 0.001,
377+
});
378+
expect(r2).toBe("result-2");
379+
expect(fn).toHaveBeenCalledTimes(2);
380+
});
357381
});
358382

359383
describe("disabled cache", () => {

0 commit comments

Comments
 (0)