From d685dc590d09557726692bccd72d83a720ababa2 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Thu, 26 Feb 2026 16:52:00 -0800 Subject: [PATCH 1/2] mono - fix: updating codecov badge --- README.md | 2 +- packages/cache-manager/README.md | 2 +- packages/cacheable-request/README.md | 2 +- packages/cacheable/README.md | 2 +- packages/file-entry-cache/README.md | 2 +- packages/flat-cache/README.md | 2 +- packages/memory/README.md | 2 +- packages/net/README.md | 2 +- packages/node-cache/README.md | 2 +- packages/node-cache/src/index.ts | 62 +++++++++++++++++--------- packages/node-cache/test/index.test.ts | 41 +++++++++++------ packages/utils/README.md | 2 +- 12 files changed, 79 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 7812bb3b..ed87a0d2 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/packages/cache-manager/README.md b/packages/cache-manager/README.md index 1b5484c2..a3029785 100644 --- a/packages/cache-manager/README.md +++ b/packages/cache-manager/README.md @@ -1,7 +1,7 @@ [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) diff --git a/packages/cacheable-request/README.md b/packages/cacheable-request/README.md index fa985655..40a52a71 100644 --- a/packages/cacheable-request/README.md +++ b/packages/cacheable-request/README.md @@ -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) diff --git a/packages/cacheable/README.md b/packages/cacheable/README.md index c4197c78..ca78dc71 100644 --- a/packages/cacheable/README.md +++ b/packages/cacheable/README.md @@ -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) diff --git a/packages/file-entry-cache/README.md b/packages/file-entry-cache/README.md index 357381d1..50f9a066 100644 --- a/packages/file-entry-cache/README.md +++ b/packages/file-entry-cache/README.md @@ -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) diff --git a/packages/flat-cache/README.md b/packages/flat-cache/README.md index 80215f17..9d7351d7 100644 --- a/packages/flat-cache/README.md +++ b/packages/flat-cache/README.md @@ -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) diff --git a/packages/memory/README.md b/packages/memory/README.md index 8845b76c..8a652571 100644 --- a/packages/memory/README.md +++ b/packages/memory/README.md @@ -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) diff --git a/packages/net/README.md b/packages/net/README.md index 5b205240..2ef91d00 100644 --- a/packages/net/README.md +++ b/packages/net/README.md @@ -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) diff --git a/packages/node-cache/README.md b/packages/node-cache/README.md index eda9fa9e..10bad526 100644 --- a/packages/node-cache/README.md +++ b/packages/node-cache/README.md @@ -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) diff --git a/packages/node-cache/src/index.ts b/packages/node-cache/src/index.ts index 77af5ed7..86a3ce19 100644 --- a/packages/node-cache/src/index.ts +++ b/packages/node-cache/src/index.ts @@ -109,10 +109,18 @@ export class NodeCache 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 */ public set(key: string | number, value: T, ttl?: number | string): boolean { // Check on key type @@ -131,15 +139,15 @@ export class NodeCache 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 + expirationTimestamp = 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 overrides stdTTL expirationTimestamp = this.resolveExpiration(ttl); } else if ( @@ -179,8 +187,15 @@ export class NodeCache 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[]} data an array of key value pairs with optional ttl - * @returns {boolean} + * @returns {boolean} true on success */ public mset(data: Array>): boolean { // Check on keys type @@ -189,14 +204,11 @@ export class NodeCache 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; } /** @@ -325,19 +337,27 @@ export class NodeCache 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. */ public ttl(key: string | number, ttl?: number | string): boolean { - // Reject negative TTL values (numeric or numeric string) - if (this.isNegativeTtl(ttl)) { - return false; - } - 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 + result.ttl = 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) { diff --git a/packages/node-cache/test/index.test.ts b/packages/node-cache/test/index.test.ts index 507fd148..8cfd976f 100644 --- a/packages/node-cache/test/index.test.ts +++ b/packages/node-cache/test/index.test.ts @@ -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); }); @@ -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 () => { diff --git a/packages/utils/README.md b/packages/utils/README.md index 6ac1bdff..4714943d 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -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) From 3752c4aba09e3025556b117252538ab6392d1097 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Fri, 27 Feb 2026 08:01:20 -0800 Subject: [PATCH 2/2] Update index.ts --- packages/node-cache/src/index.ts | 35 ++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/node-cache/src/index.ts b/packages/node-cache/src/index.ts index 86a3ce19..6516e239 100644 --- a/packages/node-cache/src/index.ts +++ b/packages/node-cache/src/index.ts @@ -121,6 +121,7 @@ export class NodeCache extends Hookified { * @param {T} value * @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. */ public set(key: string | number, value: T, ttl?: number | string): boolean { // Check on key type @@ -143,9 +144,14 @@ export class NodeCache extends Hookified { let expirationTimestamp = 0; // 0 = never delete if (this.isNegativeTtl(ttl)) { - // Negative TTL: store with a past timestamp so it expires immediately on next access - expirationTimestamp = this.getExpirationTimestamp( - typeof ttl === "string" ? Number(ttl) : (ttl as number), + // 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), + ), ); } else if (ttl !== undefined && (typeof ttl === "string" || ttl > 0)) { // Explicit positive TTL or string shorthand overrides stdTTL @@ -196,6 +202,7 @@ export class NodeCache extends Hookified { * * @param {PartialNodeCacheItem[]} data an array of key value pairs with optional ttl * @returns {boolean} true on success + * @throws {Error} If `data` is not an array, or if any item has an invalid key or ttl type. */ public mset(data: Array>): boolean { // Check on keys type @@ -348,14 +355,30 @@ export class NodeCache extends Hookified { * @param {string | number} key if the key is a number it will convert it to a string * @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). */ public ttl(key: string | number, ttl?: number | string): boolean { + // 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 (this.isNegativeTtl(ttl)) { - // Negative TTL: set past timestamp so it expires immediately on next access - result.ttl = this.getExpirationTimestamp( - typeof ttl === "string" ? Number(ttl) : (ttl as number), + // 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