Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lru-cache): support options and fetchMethod #321

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/2.drivers/lru-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,26 @@ const storage = createStorage({
driver: lruCacheDriver(),
});
```

## Stale while revalidate

LRU cache allows you to easily create a `stale-while-revalidate` storage.

```ts
const staleWhileRevalidateStorage = createStorage({
driver: driver({
max: 100,
ttl: 1000 * 60 * 15, // 15 minutes
allowStale: true,
fetchMethod: async (key, value, options) => {
const data = await fetch("https://your.api.com/endpoint?query${key}");
return data;
},
}),
});

const data = await staleWhileRevalidateStorage.get("my-search-result");
```

That approach will invoke API call maximum once per 15 minutes and will return cached values. Additionally after 15 minutes it will return stale value and will trigger API call in the background to update the cache.
It allows you for great performance and user experience when the accuracy of the returned data is not critical.
12 changes: 7 additions & 5 deletions src/drivers/lru-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default defineDriver((opts: LRUDriverOptions = {}) => {
max: 1000,
sizeCalculation:
opts.maxSize || opts.maxEntrySize
? (value, key: string) => {
? (value: unknown, key: string) => {
return key.length + byteLength(value);
}
: undefined,
Expand All @@ -29,11 +29,13 @@ export default defineDriver((opts: LRUDriverOptions = {}) => {
hasItem(key) {
return cache.has(key);
},
getItem(key) {
return cache.get(key) ?? null;
async getItem(key, options) {
const storageResult = await cache.fetch(key, options);
return storageResult ?? null;
},
getItemRaw(key) {
return cache.get(key) ?? null;
async getItemRaw(key, options) {
const storageResult = await cache.fetch(key, options);
return storageResult ?? null;
},
setItem(key, value) {
cache.set(key, value);
Expand Down
62 changes: 61 additions & 1 deletion test/drivers/lru-cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { it, describe, expect } from "vitest";
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";

Check warning on line 1 in test/drivers/lru-cache.test.ts

View workflow job for this annotation

GitHub Actions / ci

'beforeEach' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 1 in test/drivers/lru-cache.test.ts

View workflow job for this annotation

GitHub Actions / ci

'afterEach' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 1 in test/drivers/lru-cache.test.ts

View workflow job for this annotation

GitHub Actions / autofix

'beforeEach' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 1 in test/drivers/lru-cache.test.ts

View workflow job for this annotation

GitHub Actions / autofix

'afterEach' is defined but never used. Allowed unused vars must match /^_/u
import driver from "../../src/drivers/lru-cache";
import { testDriver } from "./utils";
import { createStorage } from "../../src";

describe("drivers: lru-cache", () => {
testDriver({
Expand All @@ -27,3 +28,62 @@
},
});
});

describe("drivers: lru-cache with TTL and stale-while-revalidate cache", () => {
it("should get the value from the fetchMethod and then from cache", async () => {
const fetchMethodTestSpy = vi.fn();
const storage = createStorage({
driver: driver({
max: 10,
ttl: 50,
allowStale: true,
fetchMethod: async (key, value, options) => {

Check warning on line 40 in test/drivers/lru-cache.test.ts

View workflow job for this annotation

GitHub Actions / ci

'options' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 40 in test/drivers/lru-cache.test.ts

View workflow job for this annotation

GitHub Actions / autofix

'options' is defined but never used. Allowed unused args must match /^_/u
fetchMethodTestSpy(key, value);
return "ourTestValue";
},
}),
});

const value = await storage.getItem("test");
expect(value).toBe("ourTestValue");
const secondCheck = await storage.getItem("test");
expect(secondCheck).toBe("ourTestValue");

expect(fetchMethodTestSpy).toHaveBeenCalledTimes(1);
});

it("[stale-while-revalidate] should return cached value while refreshing for the new value", async () => {
const fetchMethodTestSpy = vi.fn();
const storage = createStorage({
driver: driver({
max: 10,
ttl: 50,
allowStale: true,
fetchMethod: async (key, value, options) => {

Check warning on line 62 in test/drivers/lru-cache.test.ts

View workflow job for this annotation

GitHub Actions / ci

'options' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 62 in test/drivers/lru-cache.test.ts

View workflow job for this annotation

GitHub Actions / autofix

'options' is defined but never used. Allowed unused args must match /^_/u
fetchMethodTestSpy(key, value);
// first time when there is no cached value we return the first string
if (!value) {
return "stale-test-value";
}
return "stale-test-value-after-revalidate";
},
}),
});

// first time we're invoking the fetchMethod and get value from it
const firstRead = await storage.getItem("stale-test-key");
expect(firstRead).toBe("stale-test-value");

// when TTL expired we're still getting the value from the cache and it's refreshed under the hood
await new Promise((resolve) => setTimeout(resolve, 100));
const secondRead = await storage.getItem("stale-test-key");
expect(secondRead).toBe("stale-test-value");

// the next read after that should return refreshed value
const thirdRead = await storage.getItem("stale-test-key");
expect(thirdRead).toBe("stale-test-value-after-revalidate");

// fetchMethod should be called only twice - once on init and once after TTL expired
expect(fetchMethodTestSpy).toHaveBeenCalledTimes(2);
});
});
Loading