Skip to content

Commit

Permalink
test: Add tests and general updates
Browse files Browse the repository at this point in the history
  • Loading branch information
helloscoopa committed Sep 9, 2024
1 parent 65660fe commit ed7054f
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 158 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
package-lock.json
dist
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,28 @@ npm install run-cache
## Usage

```js
import { RunCache } from 'run-cache'
import { RunCache } from "run-cache";

// Set a cache value with 60s ttl
RunCache.set('sample_key_1', 'sample_value_1', 60000)
RunCache.set("sample_key_1", "sample_value_1", 60000);

// Set a cache value with function definition to fetch the value later
RunCache.setWithSourceFn('sample_key_2', () => { return Promise.resolve('sample_value_2') })
RunCache.setWithSourceFn("sample_key_2", () => {
return Promise.resolve("sample_value_2");
});

// Refetch the cache value (This will call the above function and update the cache value)
await RunCache.refetch('sample_key_2')
await RunCache.refetch("sample_key_2");

// Get a value for a given cache key
const value = RunCache.get('sample_key_1')
const value = RunCache.get("sample_key_1");

// Delete a specific cache key
RunCache.delete('sample_key_1');
RunCache.delete("sample_key_1");

// Delete all cache keys
RunCache.deleteAll()
RunCache.deleteAll();

// Returns a boolean based on existence of the given cache key
const hasCache = RunCache.has('sample_key_1')
```
const hasCache = RunCache.has("sample_key_1");
```
147 changes: 1 addition & 146 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,146 +1 @@
type SourceFn = () => Promise<string>;

type CacheState = {
value: string;
sourceFn?: SourceFn;
expiry?: number;
};

export class RunCache {
private static cache: Map<string, CacheState> = new Map<string, CacheState>();

private static getExpiry(ttl: number | undefined): number | undefined {
return ttl ? Date.now() + ttl : undefined;
}

/**
* Adds a value to the cache with an optional TTL.
*
* @param {string} key - The cache key.
* @param {string} value - The value to cache.
* @param {number} [ttl] - Optional time-to-live in milliseconds.
* @returns {void}
*/
static set(key: string, value: string, ttl?: number): void {
RunCache.cache.set(key, { value, expiry: this.getExpiry(ttl) });
}

/**
* Adds a value to the cache using a source function and sets an optional TTL.
* The source function is used to generate the value and is stored for future refetching.
*
* @param {string} key - The cache key.
* @param {SourceFn} sourceFn - The function used to generate the value to be cached.
* @param {number} [ttl] - Optional time-to-live in milliseconds.
* @returns {Promise<void>} A promise that resolves when the value is set.
* @throws Will throw an error if the source function fails.
*/
static async setWithSourceFn(
key: string,
sourceFn: SourceFn,
ttl?: number,
): Promise<void> {
try {
const value = await sourceFn.call(this);
RunCache.cache.set(key, {
value: JSON.stringify(value),
expiry: this.getExpiry(ttl),
sourceFn: sourceFn,
});
} catch (e) {
throw e;
}
}

/**
* Refetches the cached value using the stored source function and updates the cache with the new value.
*
* @param {string} key - The cache key.
* @param {number} [ttl] - Optional time-to-live in milliseconds for the updated value.
* @returns {Promise<void>} A promise that resolves when the value is updated.
* @throws Will throw an error if the source function fails.
*/
static async refetch(key: string, ttl?: number): Promise<void> {
const cached = RunCache.cache.get(key);

if (!cached) {
return;
}

if (!cached.sourceFn) {
return;
}

try {
const value = await cached.sourceFn.call(this);
RunCache.cache.set(key, {
value: JSON.stringify(value),
expiry: this.getExpiry(ttl),
sourceFn: cached.sourceFn,
});
} catch (e) {
throw e;
}
}

/**
* Retrieves the cached value associated with the given key if it exists and has not expired.
*
* @param {string} key - The cache key.
* @returns {string | undefined} The cached value, or undefined if not found or expired.
*/
static get(key: string): string | undefined {
const cached = RunCache.cache.get(key);

if (!cached) {
return undefined;
}

if (cached.expiry && cached.expiry < Date.now()) {
RunCache.cache.delete(key);
return undefined;
}

return cached.value;
}

/**
* Deletes the cached value associated with the given key.
*
* @param {string} key - The cache key.
* @returns {boolean} True if the key was deleted, false otherwise.
*/
static delete(key: string): boolean {
return RunCache.cache.delete(key);
}

/**
* Clears all cached values.
*
* @returns {void}
*/
static deleteAll(): void {
RunCache.cache.clear();
}

/**
* Checks if the cache contains a valid (non-expired) value for the given key.
*
* @param {string} key - The cache key.
* @returns {boolean} True if the cache contains a valid value for the key, false otherwise.
*/
static has(key: string): boolean {
const cached = RunCache.cache.get(key);

if (!cached) {
return false;
}

if (cached.expiry && cached.expiry < Date.now()) {
RunCache.cache.delete(key);
return false;
}

return true;
}
}
export { RunCache } from "./src/run-cache";
9 changes: 9 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// jest.config.js
export default {
preset: "ts-jest",
testEnvironment: "node",
transform: {
"^.+\\.tsx?$": "ts-jest", // Transform TypeScript files using ts-jest
},
extensionsToTreatAsEsm: [".ts"], // Treat .ts files as ES modules
};
17 changes: 14 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
{
"name": "run-cache",
"version": "1.0.4",
"version": "1.0.5",
"description": "`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.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jest test.ts"
},
"keywords": ["cache", "runtime", "in-memory", "memory"],
"type": "module",
"keywords": [
"cache",
"runtime",
"in-memory",
"memory"
],
"author": "helloscoopa",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/helloscoopa/run-cache.git"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-jest": "^29.2.5"
}
}
150 changes: 150 additions & 0 deletions src/run-cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { RunCache } from "./run-cache";

describe("RunCache", () => {
beforeEach(() => {
RunCache.deleteAll();
});

describe("set()", () => {
it("should throw an error if the cache key or value is empty", () => {
expect(() => RunCache.set("", "value1")).toThrow("Empty key");
expect(() => RunCache.set("key1", "")).toThrow("Empty value");
});

it("should return true if the cache value set successfully", () => {
expect(RunCache.set("key1", "value1")).toBe(true);
});

it("should return true if the cache set with a ttl and ttl is functioning properly", () => {
expect(RunCache.set("key2", "value2", 100)).toBe(true);
expect(RunCache.get("key2")).toBe("value2");

// Wait for the TTL to expire
setTimeout(() => {
expect(RunCache.get("key2")).toBeUndefined();
}, 150);
});
});

describe("get()", () => {
it("should return undefined if the key is empty", () => {
expect(RunCache.get("")).toBeUndefined();
});

it("should return undefined if the key is not found", () => {
expect(RunCache.get("key1")).toBeUndefined();
});

it("should return the value successfully", () => {
RunCache.set("key1", "value1");
expect(RunCache.get("key1")).toBe("value1");
});
});

describe("delete()", () => {
it("should return false if the operation failed", () => {
expect(RunCache.delete("nonExistentKey")).toBe(false);
});

it("should return true if the value is successfully deleted", () => {
RunCache.set("key1", "value1");
expect(RunCache.delete("key1")).toBe(true);
expect(RunCache.get("key1")).toBeUndefined();
});
});

describe("deleteAll()", () => {
it("should clear all values", () => {
RunCache.set("key1", "value1");
RunCache.set("key2", "value2");
RunCache.deleteAll();
expect(RunCache.get("key1")).toBeUndefined();
expect(RunCache.get("key2")).toBeUndefined();
});
});

describe("has()", () => {
it("should return true if the key exists", () => {
RunCache.set("key1", "value1");
expect(RunCache.has("key1")).toBe(true);
});

it("should return false if the key exists", () => {
expect(RunCache.has("nonExistentKey")).toBe(false);
});

it("should return false after ttl expiry", () => {
RunCache.set("key2", "value2", 50); // Set TTL to 50ms
expect(RunCache.has("key2")).toBe(true);

// Wait for the TTL to expire
setTimeout(() => {
expect(RunCache.has("key2")).toBe(false);
}, 100);
});
});

describe("setWithSourceFn()", () => {
it("should throw an error when the source function throws an error", async () => {
let sourceFn = async () => {
throw Error("Unexpected Error");
};
expect(RunCache.setWithSourceFn("key1", sourceFn)).rejects.toThrow(
"Source function failed",
);
});

it("should be able to set a value with source function successfully", async () => {
const sourceFn = async () => "dynamicValue";
await RunCache.setWithSourceFn("key2", sourceFn);
expect(RunCache.get("key2")).toBe('"dynamicValue"');
});
});

describe("refetch()", () => {
it("should throw an error if refetch is called on a key having no source function", async () => {
RunCache.set("key2", "value2");
await expect(RunCache.refetch("key2")).rejects.toThrow(
"No source function found",
);
});

it("should throw an error when the source function throws an error", async () => {
let breaker = false;

let sourceFn = async () => {
if (breaker) {
throw Error("Unexpected Error");
} else {
return "SomeValue";
}
};
await RunCache.setWithSourceFn("key3", sourceFn);

// Make source function to fail
breaker = true;

expect(RunCache.refetch("key3")).rejects.toThrow(
"Source function failed",
);
});

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

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

await RunCache.setWithSourceFn("key1", sourceFn);
expect(RunCache.get("key1")).toBe('"initialValue"');

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

await RunCache.refetch("key1");
expect(RunCache.get("key1")).toBe('"updatedValue"');
});
});
});
Loading

0 comments on commit ed7054f

Please sign in to comment.