Skip to content

Commit

Permalink
fix: Disable concurrent sourceFn calls between refetches (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
helloscoopa authored Sep 14, 2024
1 parent 67748a8 commit 90ca9a4
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 36 deletions.
15 changes: 15 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "en-US"
early_access: false
reviews:
profile: "assertive"
request_changes_workflow: true
high_level_summary: true
poem: false
review_status: true
collapse_walkthrough: false
auto_review:
enabled: true
drafts: false
chat:
auto_reply: true
38 changes: 19 additions & 19 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,28 @@ name: Run Tests

on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Test
run: npm test
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
registry-url: "https://registry.npmjs.org"

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Test
run: npm test
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[![npm](https://img.shields.io/npm/v/run-cache?colorA=000000&colorB=ff98a2)](https://www.npmjs.com/package/run-cache)

# RunCache
# Run~time~Cache

RunCache is a dependency nil, light-weight in-memory caching library for JavaScript and TypeScript that allows you to cache `string` values with optional time-to-live (TTL) settings. It also supports caching values generated from asynchronous functions and provides methods to refetch them on demand.

Expand Down
86 changes: 71 additions & 15 deletions src/run-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,46 +32,57 @@ describe("RunCache", () => {
});

it("should throw an error when the source function throws an error", async () => {
const sourceFn = async () => {
const sourceFn = jest.fn(async () => {
throw Error("Unexpected Error");
};
});
await expect(
RunCache.set({
key: "key1",
sourceFn,
}),
).rejects.toThrow("Source function failed for key: 'key1'");

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

it("should throw an error when the autoRefetch: true while ttl is not provided", async () => {
const sourceFn = async () => "dynamicValue";
const sourceFn = jest.fn(async () => "dynamicValue");

await expect(
RunCache.set({
key: "key2",
sourceFn,
autoRefetch: true,
}),
).rejects.toThrow("`autoRefetch` is not allowed without a `ttl`");

expect(sourceFn).toHaveBeenCalledTimes(0);
});

it("should be able to set a value with source function successfully", async () => {
const sourceFn = async () => "dynamicValue";
const sourceFn = jest.fn(async () => "dynamicValue");

await RunCache.set({
key: "key2",
sourceFn,
});
expect(await RunCache.get("key2")).toBe(JSON.stringify("dynamicValue"));

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

it("should be able to set a value with source function, autoRefetch enabled successfully", async () => {
const sourceFn = async () => "dynamicValue";
const sourceFn = jest.fn(async () => "dynamicValue");

await RunCache.set({
key: "key2",
sourceFn,
ttl: 100,
autoRefetch: true,
});
expect(await RunCache.get("key2")).toBe(JSON.stringify("dynamicValue"));

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

it("should return true if the cache value set successfully", async () => {
Expand Down Expand Up @@ -101,7 +112,7 @@ describe("RunCache", () => {
});

it("should return the value successfully if the cache is not expired", async () => {
const sourceFn = async () => "value1";
const sourceFn = jest.fn(async () => "value1");

await RunCache.set({
key: "key1",
Expand All @@ -110,12 +121,14 @@ describe("RunCache", () => {
});

expect(await RunCache.get("key1")).toBe(JSON.stringify("value1"));

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

it("should auto refetch and return the new value successfully", async () => {
let dynamicValue = "initialValue";

const sourceFn = async () => dynamicValue;
const sourceFn = jest.fn(async () => dynamicValue);

await RunCache.set({
key: "key2",
Expand All @@ -124,6 +137,8 @@ describe("RunCache", () => {
ttl: 100,
});

expect(sourceFn).toHaveBeenCalledTimes(1);

expect(await RunCache.get("key2")).toBe(JSON.stringify("initialValue"));

dynamicValue = "updatedValue";
Expand All @@ -132,6 +147,8 @@ describe("RunCache", () => {
await sleep(150);

expect(await RunCache.get("key2")).toBe(JSON.stringify("updatedValue"));

expect(sourceFn).toHaveBeenCalledTimes(2);
});

it("should return the value successfully", async () => {
Expand Down Expand Up @@ -212,61 +229,100 @@ describe("RunCache", () => {
it("should throw an error when the source function throws an error", async () => {
let shouldThrowError = false;

const sourceFn = async () => {
const sourceFn = jest.fn(async () => {
if (shouldThrowError) {
throw Error("Unexpected Error");
} else {
return "SomeValue";
}
};
});
await RunCache.set({ key: "key3", sourceFn });

expect(sourceFn).toHaveBeenCalledTimes(1);

// Make source function to fail
shouldThrowError = true;

expect(RunCache.refetch("key3")).rejects.toThrow(
"Source function failed for key: 'key3'",
);

expect(sourceFn).toHaveBeenCalledTimes(2);
});

it("should not refetch if the key does not exist", async () => {
await expect(RunCache.refetch("nonExistentKey")).resolves.toBeFalsy();
});

it("should not call sourceFn more than once at a time", async () => {
const sourceFn = jest.fn(async () => {
await sleep(1000);
return "value";
});

await RunCache.set({ key: "key1", value: "value1", sourceFn });

const [firstRefetch, secondRefetch, thirdRefetch] = await Promise.all([
RunCache.refetch("key1"),
RunCache.refetch("key1"),
RunCache.refetch("key1"),
]);

expect(firstRefetch).toBeTruthy();
expect(secondRefetch).toBeFalsy();
expect(thirdRefetch).toBeFalsy();

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

it("should refetch and update the value from the source function", async () => {
let dynamicValue = "initialValue";
const sourceFn = async () => dynamicValue;
const sourceFn = jest.fn(async () => dynamicValue);

await RunCache.set({ key: "key1", sourceFn });
expect(await RunCache.get("key1")).toBe(JSON.stringify("initialValue"));

expect(sourceFn).toHaveBeenCalledTimes(1);

// Update what's being returned in the source function
dynamicValue = "updatedValue";

await RunCache.refetch("key1");

expect(sourceFn).toHaveBeenCalledTimes(2);

expect(await RunCache.get("key1")).toBe(JSON.stringify("updatedValue"));
});

it("should trigger onRefetch event on refetch", async () => {
let dynamicValue = "initialValue";
const sourceFn = async () => dynamicValue;
const sourceFn = jest.fn(async () => dynamicValue);

const funcToBeExecutedOnRefetch = async (cacheState: EventParam) => {
expect(cacheState.key).toBe("key2");
expect(cacheState.value).toBe(JSON.stringify("updatedValue"));
};
const funcToBeExecutedOnRefetch = jest.fn(
async (cacheState: EventParam) => {
expect(cacheState.key).toBe("key2");
expect(cacheState.value).toBe(JSON.stringify("updatedValue"));
},
);

await RunCache.set({
key: "key2",
sourceFn,
onRefetch: funcToBeExecutedOnRefetch,
});

expect(sourceFn).toHaveBeenCalledTimes(1);
expect(funcToBeExecutedOnRefetch).toHaveBeenCalledTimes(0);

expect(await RunCache.get("key2")).toBe(JSON.stringify("initialValue"));

// Update what's being returned in the source function
dynamicValue = "updatedValue";

await RunCache.refetch("key2");

expect(sourceFn).toHaveBeenCalledTimes(2);
expect(funcToBeExecutedOnRefetch).toHaveBeenCalledTimes(1);
});
});
});
17 changes: 16 additions & 1 deletion src/run-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type CacheState = {
updateAt: number;
ttl?: number;
autoRefetch?: boolean;
fetching?: boolean;
sourceFn?: SourceFn;

onRefetch?: EventFn;
Expand Down Expand Up @@ -146,7 +147,13 @@ class RunCache {
throw Error(`No source function found for key: '${key}'`);
}

if (cached.fetching) {
return false;
}

try {
RunCache.cache.set(key, { fetching: true, ...cached });

const value = await cached.sourceFn();

const refetchedCache = {
Expand All @@ -167,10 +174,18 @@ class RunCache {
});
}

RunCache.cache.set(key, refetchedCache);
RunCache.cache.set(key, {
fetching: undefined,
...refetchedCache,
});

return true;
} catch (e) {
RunCache.cache.set(key, {
fetching: undefined,
...cached,
});

throw Error(`Source function failed for key: '${key}'`);
}
}
Expand Down

0 comments on commit 90ca9a4

Please sign in to comment.