Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> Caching for Nodejs based on Keyv

[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/branch/main/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![license](https://img.shields.io/github/license/jaredwray/cacheable)](https://github.com/jaredwray/cacheable/blob/main/LICENSE)

`Cacheable` provides a robust, scalable, and maintained set of caching packages that can be used in various projects. The packages in this repository are:
Expand Down
2 changes: 1 addition & 1 deletion packages/cache-manager/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[<img align="center" src="https://cacheable.org/symbol.svg" alt="Cacheable" />](https://github.com/jaredwray/cacheable)

# cache-manager
[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/branch/main/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![npm](https://img.shields.io/npm/dm/cache-manager)](https://npmjs.com/package/cache-manager)
[![npm](https://img.shields.io/npm/v/cache-manager)](https://npmjs.com/package/cache-manager)
Expand Down
2 changes: 1 addition & 1 deletion packages/cacheable-request/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

> Wrap native HTTP requests with RFC compliant cache support

[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/branch/main/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![npm](https://img.shields.io/npm/dm/cacheable-request.svg)](https://www.npmjs.com/package/cacheable-request)
[![npm](https://img.shields.io/npm/v/cacheable-request.svg)](https://www.npmjs.com/package/cacheable-request)
Expand Down
2 changes: 1 addition & 1 deletion packages/cacheable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> High Performance Layer 1 / Layer 2 Caching with Keyv Storage

[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/branch/main/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![npm](https://img.shields.io/npm/dm/cacheable.svg)](https://www.npmjs.com/package/cacheable)
[![npm](https://img.shields.io/npm/v/cacheable)](https://www.npmjs.com/package/cacheable)
Expand Down
2 changes: 1 addition & 1 deletion packages/file-entry-cache/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# file-entry-cache
> A lightweight cache for file metadata, ideal for processes that work on a specific set of files and only need to reprocess files that have changed since the last run

[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/branch/main/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![npm](https://img.shields.io/npm/dm/file-entry-cache.svg)](https://www.npmjs.com/package/file-entry-cache)
[![npm](https://img.shields.io/npm/v/file-entry-cache)](https://www.npmjs.com/package/file-entry-cache)
Expand Down
2 changes: 1 addition & 1 deletion packages/flat-cache/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# flat-cache
> A simple key/value storage using files to persist the data

[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/branch/main/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![npm](https://img.shields.io/npm/dm/flat-cache.svg)](https://www.npmjs.com/package/flat-cache)
[![npm](https://img.shields.io/npm/v/flat-cache)](https://www.npmjs.com/package/flat-cache)
Expand Down
2 changes: 1 addition & 1 deletion packages/memory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> High Performance Layer 1 / Layer 2 Caching with Keyv Storage

[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/branch/main/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![npm](https://img.shields.io/npm/dm/@cacheable/memory.svg)](https://www.npmjs.com/package/@cacheable/memory)
[![npm](https://img.shields.io/npm/v/@cacheable/memory.svg)](https://www.npmjs.com/package/@cacheable/memory)
Expand Down
2 changes: 1 addition & 1 deletion packages/net/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> High Performance Network Caching for Node.js with fetch support and HTTP cache semantics

[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/branch/main/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![npm](https://img.shields.io/npm/dm/@cacheable/net.svg)](https://www.npmjs.com/package/@cacheable/net)
[![npm](https://img.shields.io/npm/v/@cacheable/net.svg)](https://www.npmjs.com/package/@cacheable/net)
Expand Down
2 changes: 1 addition & 1 deletion packages/node-cache/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

> Simple and Maintained fast Node.js caching

[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/branch/main/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![npm](https://img.shields.io/npm/dm/@cacheable/node-cache.svg)](https://www.npmjs.com/package/@cacheable/node-cache)
[![npm](https://img.shields.io/npm/v/@cacheable/node-cache)](https://www.npmjs.com/package/@cacheable/node-cache)
Expand Down
81 changes: 62 additions & 19 deletions packages/node-cache/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,19 @@ export class NodeCache<T> extends Hookified {

/**
* Sets a key value pair. It is possible to define a ttl (in seconds). Returns true on success.
*
* TTL behavior:
* - `ttl > 0`: cache expires after the given number of seconds
* - `ttl === 0`: cache indefinitely (overrides stdTTL)
* - `ttl < 0`: store the value but it expires immediately on next access (matches original node-cache behavior)
* - `ttl` omitted/undefined: use `stdTTL` from options (0 = unlimited if stdTTL is 0 or not set)
* - `ttl` as string: shorthand format like '1h', '30s', '5m', '2d'
*
* @param {string | number} key - it will convert the key to a string
* @param {T} value
* @param {number | string} [ttl] - this is in seconds and undefined will use the default ttl
* @returns {boolean}
* @param {number | string} [ttl] - TTL in seconds. 0 = unlimited, negative = expires immediately, string = shorthand format
* @returns {boolean} true on success
* @throws {Error} If the `key` or `ttl` is of an invalid type.
*/
Comment thread
jaredwray marked this conversation as resolved.
public set(key: string | number, value: T, ttl?: number | string): boolean {
// Check on key type
Expand All @@ -131,15 +140,20 @@ export class NodeCache<T> extends Hookified {
throw this.createError(NodeCacheErrors.ETTLTYPE, this.formatKey(key));
}

// Reject negative TTL values (numeric or numeric string)
if (this.isNegativeTtl(ttl)) {
return false;
}

const keyValue = this.formatKey(key);
let expirationTimestamp = 0; // 0 = never delete

if (ttl !== undefined && (typeof ttl === "string" || ttl > 0)) {
if (this.isNegativeTtl(ttl)) {
// Negative TTL: store with a past timestamp so it expires immediately on next access.
// Math.max(1, ...) ensures the timestamp is always > 0, since the expiration
// check only triggers for ttl > 0 (0 means "unlimited").
expirationTimestamp = Math.max(
1,
this.getExpirationTimestamp(
typeof ttl === "string" ? Number(ttl) : (ttl as number),
),
);
Comment thread
jaredwray marked this conversation as resolved.
} else if (ttl !== undefined && (typeof ttl === "string" || ttl > 0)) {
// Explicit positive TTL or string shorthand overrides stdTTL
expirationTimestamp = this.resolveExpiration(ttl);
} else if (
Expand Down Expand Up @@ -179,8 +193,16 @@ export class NodeCache<T> extends Hookified {

/**
* Sets multiple key val pairs. It is possible to define a ttl (seconds). Returns true on success.
*
* Each item follows the same TTL behavior as `set()`:
* - Positive TTL: expires after the given seconds
* - `0`: cache indefinitely
* - Negative TTL: stored but expires immediately on next access
* - Omitted: uses `stdTTL` from options
*
* @param {PartialNodeCacheItem<T>[]} data an array of key value pairs with optional ttl
* @returns {boolean}
* @returns {boolean} true on success
* @throws {Error} If `data` is not an array, or if any item has an invalid key or ttl type.
*/
Comment thread
jaredwray marked this conversation as resolved.
public mset(data: Array<PartialNodeCacheItem<T>>): boolean {
// Check on keys type
Expand All @@ -189,14 +211,11 @@ export class NodeCache<T> extends Hookified {
throw this.createError(NodeCacheErrors.EKEYSTYPE);
}

let success = true;
for (const item of data) {
if (!this.set(item.key, item.value, item.ttl)) {
success = false;
}
this.set(item.key, item.value, item.ttl);
}

return success;
return true;
}

/**
Expand Down Expand Up @@ -325,19 +344,43 @@ export class NodeCache<T> extends Hookified {
/**
* Redefine the ttl of a key. Returns true if the key has been found and changed.
* Otherwise returns false. If the ttl-argument isn't passed the default-TTL will be used.
*
* TTL behavior:
* - `ttl > 0`: key expires after the given number of seconds
* - `ttl === 0`: key lives indefinitely (overrides stdTTL)
* - `ttl < 0`: key expires immediately on next access (matches original node-cache behavior)
* - `ttl` omitted/undefined: use `stdTTL` from options (0 = unlimited if stdTTL is 0 or not set)
* - `ttl` as string: shorthand format like '1h', '30s', '5m', '2d'
*
* @param {string | number} key if the key is a number it will convert it to a string
* @param {number | string} [ttl] the ttl in seconds if number, or a shorthand string like '1h' for 1 hour
* @param {number | string} [ttl] TTL in seconds. 0 = unlimited, negative = expires immediately, string = shorthand format
* @returns {boolean} true if the key has been found and changed. Otherwise returns false.
* @throws {Error} If the `ttl` is of an invalid type (must be a number or string).
*/
Comment thread
jaredwray marked this conversation as resolved.
public ttl(key: string | number, ttl?: number | string): boolean {
// Reject negative TTL values (numeric or numeric string)
if (this.isNegativeTtl(ttl)) {
return false;
// Check on ttl type
/* v8 ignore next -- @preserve */
if (
ttl !== undefined &&
typeof ttl !== "number" &&
typeof ttl !== "string"
) {
throw this.createError(NodeCacheErrors.ETTLTYPE, this.formatKey(key));
}

const result = this.store.get(this.formatKey(key));
if (result) {
if (ttl !== undefined && (typeof ttl === "string" || ttl > 0)) {
if (this.isNegativeTtl(ttl)) {
// Negative TTL: set past timestamp so it expires immediately on next access.
// Math.max(1, ...) ensures the timestamp is always > 0, since the expiration
// check only triggers for ttl > 0 (0 means "unlimited").
result.ttl = Math.max(
1,
this.getExpirationTimestamp(
typeof ttl === "string" ? Number(ttl) : (ttl as number),
),
);
} else if (ttl !== undefined && (typeof ttl === "string" || ttl > 0)) {
// Explicit positive TTL or string shorthand
result.ttl = this.resolveExpiration(ttl);
} else if (ttl === 0) {
Expand Down
41 changes: 28 additions & 13 deletions packages/node-cache/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,19 @@ describe("NodeCache", () => {
expect(cache.get("baz")).toBe("qux");
});

test("should return false from mset when any item has a negative ttl", () => {
test("should store items with negative ttl in mset but they expire immediately on access", () => {
const cache = new NodeCache({ checkperiod: 0 });
const list = [
{ key: "good", value: "ok" },
{ key: "bad", value: "nope", ttl: -1 },
];
const result = cache.mset(list);
expect(result).toBe(false);
expect(result).toBe(true);
expect(cache.get("good")).toBe("ok");
// Negative TTL item is stored but expires immediately on get
expect(cache.has("bad")).toBe(true);
expect(cache.get("bad")).toBe(undefined);
// After get triggers expiration + deleteOnExpire, key is removed
expect(cache.has("bad")).toBe(false);
});

Expand Down Expand Up @@ -256,35 +260,46 @@ describe("NodeCache", () => {
expect(cache.get("unlimited")).toBe("stays");
});

test("should return false and not store key when ttl is negative", () => {
test("should store key with negative ttl but it expires immediately on access", () => {
const cache = new NodeCache({ checkperiod: 0 });
const result = cache.set("foo", "bar", -1);
expect(result).toBe(false);
expect(cache.has("foo")).toBe(false);
expect(result).toBe(true);
// Key is stored with a past expiration timestamp
expect(cache.has("foo")).toBe(true);
// get() sees it's expired and returns undefined
expect(cache.get("foo")).toBe(undefined);
// After get triggers expiration + deleteOnExpire, key is removed
expect(cache.has("foo")).toBe(false);
});

test("should return false on ttl() method when ttl is negative", () => {
test("should expire key immediately when ttl() is called with negative ttl", () => {
const cache = new NodeCache({ checkperiod: 0 });
cache.set("foo", "bar");
const result = cache.ttl("foo", -1);
expect(result).toBe(false);
expect(cache.get("foo")).toBe("bar");
expect(result).toBe(true);
// Key exists but will expire on next access
expect(cache.has("foo")).toBe(true);
expect(cache.get("foo")).toBe(undefined);
expect(cache.has("foo")).toBe(false);
});

test("should reject negative TTL passed as a numeric string in set()", () => {
test("should store but expire immediately when negative TTL is passed as a numeric string in set()", () => {
const cache = new NodeCache({ checkperiod: 0 });
const result = cache.set("foo", "bar", "-1");
expect(result).toBe(false);
expect(result).toBe(true);
expect(cache.has("foo")).toBe(true);
expect(cache.get("foo")).toBe(undefined);
expect(cache.has("foo")).toBe(false);
});

test("should reject negative TTL passed as a numeric string in ttl()", () => {
test("should expire immediately when negative TTL is passed as a numeric string in ttl()", () => {
const cache = new NodeCache({ checkperiod: 0 });
cache.set("foo", "bar");
const result = cache.ttl("foo", "-5");
expect(result).toBe(false);
expect(cache.get("foo")).toBe("bar");
expect(result).toBe(true);
expect(cache.has("foo")).toBe(true);
expect(cache.get("foo")).toBe(undefined);
expect(cache.has("foo")).toBe(false);
});

test("should set unlimited expiration on ttl() method when ttl is 0", async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Cacheble Utils

[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![codecov](https://codecov.io/gh/jaredwray/cacheable/branch/main/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![npm](https://img.shields.io/npm/dm/@cacheable/utils.svg)](https://www.npmjs.com/package/@cacheable/utils)
[![npm](https://img.shields.io/npm/v/@cacheable/utils)](https://www.npmjs.com/package/@cacheable/utils)
Expand Down