-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
65660fe
commit ed7054f
Showing
8 changed files
with
360 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
package-lock.json | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"'); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.