From 4a82f84ee40c1b3b63996f188b3b37f409015b97 Mon Sep 17 00:00:00 2001 From: mistval Date: Thu, 9 Oct 2025 18:45:44 -0700 Subject: [PATCH 1/5] clear cache function --- README.md | 10 +- package-lock.json | 571 ++++++++++------------- package.json | 4 +- src/classes/caching/file_system_cache.ts | 4 + test/tests.ts | 14 + 5 files changed, 280 insertions(+), 323 deletions(-) diff --git a/README.md b/README.md index b8e0e90..dc160a6 100644 --- a/README.md +++ b/README.md @@ -85,13 +85,15 @@ Options: // Specify where to keep the cache. If undefined, '.cache' is used by default. // If this directory does not exist, it will be created. cacheDirectory: '/my/cache/directory/path', - // Time to live. How long (in ms) responses remain cached before being - // automatically ejected. If undefined, responses are never - // automatically ejected from the cache. + // Time to live. How long (in ms) responses remain cached before + // becoming invalid. If undefined, cached responses never become + // invalid. ttl: 1000, } - ``` + +If you set a TTL, be aware that cache entries are not actively deleted from disk when they become invalid, which can cause disk bloat over time. You can clear the entire cache off of the disk by calling `.clear()` on it. + ### Cache with Redis Use the [@node-fetch-cache/redis](https://www.npmjs.com/package/@node-fetch-cache/redis) package to cache in Redis. diff --git a/package-lock.json b/package-lock.json index 1a28ebb..210eb34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "node-fetch-cache", - "version": "5.0.2", + "version": "5.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-fetch-cache", - "version": "5.0.2", + "version": "5.1.0", "license": "MIT", "dependencies": { - "cacache": "^18.0.4", + "cacache": "^20.0.1", "formdata-node": "^6.0.3", "locko": "^1.1.0", "node-fetch": "3.3.2" @@ -569,6 +569,27 @@ "dev": true, "peer": true }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -728,20 +749,22 @@ } }, "node_modules/@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "license": "ISC", "dependencies": { "semver": "^7.3.5" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "optional": true, "engines": { "node": ">=14" @@ -1222,18 +1245,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1314,7 +1325,8 @@ "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -1326,10 +1338,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1445,42 +1458,34 @@ } }, "node_modules/cacache": { - "version": "18.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", - "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.1.tgz", + "integrity": "sha512-+7LYcYGBYoNqTp1Rv7Ny1YjUo5E0/ftkQtraH3vkfAGgVHc+ouWdC8okAwQgQR7EVIdW6JTzTmhKFwzb+4okAQ==", "license": "ISC", "dependencies": { - "@npmcli/fs": "^3.1.0", + "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", + "glob": "^11.0.3", + "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "unique-filename": "^4.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/cacache/node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -1502,74 +1507,97 @@ } }, "node_modules/cacache/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", "engines": { - "node": "14 || >=16.14" + "node": "20 || >=22" } }, "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/cacache/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/cacache/node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "node_modules/cacache/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", "dependencies": { - "aggregate-error": "^3.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=10" + "node": "20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/cacache/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", "engines": { "node": ">=14" }, @@ -1630,22 +1658,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "engines": { - "node": ">=6" - } - }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -2225,17 +2237,6 @@ "node": ">=12.20.0" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2438,14 +2439,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "engines": { - "node": ">=8" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2603,6 +2596,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -2815,29 +2809,6 @@ "node": ">=8" } }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mocha": { "version": "10.8.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", @@ -2875,9 +2846,9 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3049,6 +3020,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3092,6 +3081,7 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -3107,6 +3097,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -3115,6 +3106,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -3260,10 +3252,11 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3486,20 +3479,22 @@ } }, "node_modules/ssri": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", - "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/ssri/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -3578,30 +3573,6 @@ "node": ">=8" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -3714,25 +3685,27 @@ "dev": true }, "node_modules/unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "license": "ISC", "dependencies": { - "unique-slug": "^4.0.0" + "unique-slug": "^5.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/uri-js": { @@ -4184,6 +4157,19 @@ "dev": true, "peer": true }, + "@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==" + }, + "@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "requires": { + "@isaacs/balanced-match": "^4.0.1" + } + }, "@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -4297,9 +4283,9 @@ } }, "@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "requires": { "semver": "^7.3.5" } @@ -4308,6 +4294,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "optional": true }, "@rollup/rollup-android-arm-eabi": { @@ -4592,15 +4579,6 @@ "peer": true, "requires": {} }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4658,7 +4636,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "binary-extensions": { "version": "2.2.0", @@ -4667,9 +4646,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -4761,38 +4740,29 @@ } }, "cacache": { - "version": "18.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", - "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.1.tgz", + "integrity": "sha512-+7LYcYGBYoNqTp1Rv7Ny1YjUo5E0/ftkQtraH3vkfAGgVHc+ouWdC8okAwQgQR7EVIdW6JTzTmhKFwzb+4okAQ==", "requires": { - "@npmcli/fs": "^3.1.0", + "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", + "glob": "^11.0.3", + "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "unique-filename": "^4.0.0" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, "foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "requires": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, @@ -4805,41 +4775,51 @@ } }, "glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + } + }, + "jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "requires": { + "@isaacs/cliui": "^8.0.2" } }, "lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==" + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==" }, "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "requires": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" } }, "minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==" + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "requires": { - "aggregate-error": "^3.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" } }, "signal-exit": { @@ -4882,16 +4862,6 @@ "readdirp": "~3.6.0" } }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" - }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -5320,14 +5290,6 @@ "fetch-blob": "^3.1.2" } }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5464,11 +5426,6 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5586,6 +5543,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, "requires": { "@isaacs/cliui": "^8.0.2", "@pkgjs/parseargs": "^0.11.0" @@ -5745,20 +5703,6 @@ "minipass": "^3.0.0" } }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, "mocha": { "version": "10.8.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", @@ -5788,9 +5732,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -5904,6 +5848,16 @@ "p-limit": "^3.0.2" } }, + "p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==" + }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5935,6 +5889,7 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, "requires": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -5943,12 +5898,14 @@ "lru-cache": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==" + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true }, "minipass": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==" + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true } } }, @@ -6037,9 +5994,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -6181,17 +6138,17 @@ "dev": true }, "ssri": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", - "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "requires": { "minipass": "^7.0.3" }, "dependencies": { "minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==" + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" } } }, @@ -6246,26 +6203,6 @@ "has-flag": "^4.0.0" } }, - "tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "dependencies": { - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" - } - } - }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6341,17 +6278,17 @@ "dev": true }, "unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "requires": { - "unique-slug": "^4.0.0" + "unique-slug": "^5.0.0" } }, "unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", "requires": { "imurmurhash": "^0.1.4" } diff --git a/package.json b/package.json index a48582a..3a3f215 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-fetch-cache", - "version": "5.0.2", + "version": "5.1.0", "description": "node-fetch with caching.", "main": "src/index.js", "type": "module", @@ -53,7 +53,7 @@ "typescript": "^5.5.4" }, "dependencies": { - "cacache": "^18.0.4", + "cacache": "^20.0.1", "formdata-node": "^6.0.3", "locko": "^1.1.0", "node-fetch": "3.3.2" diff --git a/src/classes/caching/file_system_cache.ts b/src/classes/caching/file_system_cache.ts index 2cb9b80..8febf3e 100644 --- a/src/classes/caching/file_system_cache.ts +++ b/src/classes/caching/file_system_cache.ts @@ -20,6 +20,10 @@ export class FileSystemCache implements INodeFetchCacheCache { this.cacheDirectory = options.cacheDirectory ?? '.cache'; } + clear() { + return cacache.rm.all(this.cacheDirectory); + } + async get(key: string, options?: { ignoreExpiration?: boolean }) { const cachedObjectInfo = await cacache.get.info(this.cacheDirectory, key); diff --git a/test/tests.ts b/test/tests.ts index b07a1f0..59493ea 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -579,6 +579,20 @@ describe('File system cache tests', () => { assert.strictEqual(response.returnedFromCache, false); }); + it('Can be cleared', async () => { + const cache = new FileSystemCache(); + defaultCachedFetch = FetchCache.create({ cache }); + let response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + + await cache.clear(); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + }); + it('Can get PNG buffer body', async () => { defaultCachedFetch = FetchCache.create({ cache: new FileSystemCache() }); response = await defaultCachedFetch(PNG_BODY_URL); From 6b6a0a349d0da009c55ac34ca1f760dc2f81af2f Mon Sep 17 00:00:00 2001 From: mistval Date: Thu, 9 Oct 2025 18:47:46 -0700 Subject: [PATCH 2/5] wording --- README.md | 2 +- test/tests.ts | 803 -------------------------------------------------- 2 files changed, 1 insertion(+), 804 deletions(-) delete mode 100644 test/tests.ts diff --git a/README.md b/README.md index dc160a6..3e2c3e4 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Options: } ``` -If you set a TTL, be aware that cache entries are not actively deleted from disk when they become invalid, which can cause disk bloat over time. You can clear the entire cache off of the disk by calling `.clear()` on it. +If you set a TTL, be aware that cache entries are not actively deleted from disk when they become invalid, which can cause disk bloat over time. To clean that up, you can clear the entire cache directory by calling `.clear()` on an instance of `FileSystemCache`. ### Cache with Redis diff --git a/test/tests.ts b/test/tests.ts deleted file mode 100644 index 59493ea..0000000 --- a/test/tests.ts +++ /dev/null @@ -1,803 +0,0 @@ -// eslint-disable-next-line import/no-unassigned-import,import/order -import 'dotenv/config.js'; -import path, { dirname } from 'path'; -import util from 'util'; -import { fileURLToPath } from 'url'; -import fs from 'fs'; -import assert from 'assert'; -import { Agent } from 'http'; -import { rimraf } from 'rimraf'; -import { FormData } from 'formdata-node'; -import standardFetch, { Request as StandardFetchRequest } from 'node-fetch'; -import FetchCache, { - MemoryCache, - FileSystemCache, - cacheStrategies, - FetchResource, - NFCResponse, - calculateCacheKey, - ISynchronizationStrategy, -} from '../src/index.js'; - -const httpBinBaseUrl = 'http://localhost:3000'; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const wait = util.promisify(setTimeout); - -const CACHE_PATH = path.join(__dirname, '..', '.cache'); -const expectedPngBuffer = fs.readFileSync(path.join(__dirname, 'expected_png.png')); - -const TWO_HUNDRED_URL = `${httpBinBaseUrl}/status/200`; -const FOUR_HUNDRED_URL = `${httpBinBaseUrl}/status/400`; -const FIVE_HUNDRED_URL = `${httpBinBaseUrl}/status/500`; -const THREE_HUNDRED_TWO_URL = `${httpBinBaseUrl}/status/302`; -const TEXT_BODY_URL = `${httpBinBaseUrl}/robots.txt`; -const JSON_BODY_URL = `${httpBinBaseUrl}/json`; -const PNG_BODY_URL = `${httpBinBaseUrl}/image/png`; -const HUNDRED_THOUSAND_BYTES_URL = `${httpBinBaseUrl}/stream-bytes/100000?chunk_size=10000`; - -const TEXT_BODY_EXPECTED = 'User-agent: *\nDisallow: /deny\n'; -const JSON_BODY_EXPECTED = `{ - "slideshow": { - "author": "Yours Truly", - "date": "date of publication", - "slides": [ - { - "title": "Wake up to WonderWidgets!", - "type": "all" - }, - { - "items": [ - "Why WonderWidgets are great", - "Who buys WonderWidgets" - ], - "title": "Overview", - "type": "all" - } - ], - "title": "Sample Slide Show" - } -} -`; - -let defaultCachedFetch: typeof FetchCache; -let defaultCache: MemoryCache; - -function post(body: string | URLSearchParams | FormData | fs.ReadStream) { - return { method: 'POST', body }; -} - -function removeDates(arrayOrObject: { date?: unknown } | string[] | string[][]) { - if (Array.isArray(arrayOrObject)) { - if (Array.isArray(arrayOrObject[0])) { - return arrayOrObject.filter(element => element[0] !== 'date'); - } - - return (arrayOrObject as string[]).filter(element => !Date.parse(element)); - } - - if (arrayOrObject.date) { - const copy = { ...arrayOrObject }; - delete copy.date; - return copy; - } - - return arrayOrObject; -} - -async function dualFetch(...args: Parameters) { - const [cachedFetchResponse, standardFetchResponse] = await Promise.all([ - defaultCachedFetch(...args), - standardFetch(...args), - ]); - - return { cachedFetchResponse, standardFetchResponse }; -} - -beforeEach(async () => { - rimraf.sync(CACHE_PATH); - defaultCache = new MemoryCache(); - defaultCachedFetch = FetchCache.create({ cache: defaultCache }); -}); - -let response: NFCResponse; - -describe('Basic property tests', () => { - it('Has a status property', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); - assert.strictEqual(cachedFetchResponse.status, standardFetchResponse.status); - - cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(cachedFetchResponse.status, standardFetchResponse.status); - }); - - it('Has a statusText property', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); - assert.strictEqual(cachedFetchResponse.statusText, standardFetchResponse.statusText); - - cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(cachedFetchResponse.statusText, standardFetchResponse.statusText); - }); - - it('Has a url property', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); - assert.strictEqual(cachedFetchResponse.url, standardFetchResponse.url); - - cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(cachedFetchResponse.url, standardFetchResponse.url); - }); - - it('Has an ok property', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(FOUR_HUNDRED_URL); - assert.strictEqual(cachedFetchResponse.ok, standardFetchResponse.ok); - assert.strictEqual(cachedFetchResponse.status, standardFetchResponse.status); - - cachedFetchResponse = await defaultCachedFetch(FOUR_HUNDRED_URL); - assert.strictEqual(cachedFetchResponse.ok, standardFetchResponse.ok); - assert.strictEqual(cachedFetchResponse.status, standardFetchResponse.status); - }); - - it('Has a redirected property', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(THREE_HUNDRED_TWO_URL); - assert.strictEqual(cachedFetchResponse.redirected, standardFetchResponse.redirected); - - cachedFetchResponse = await defaultCachedFetch(THREE_HUNDRED_TWO_URL); - assert.strictEqual(cachedFetchResponse.redirected, standardFetchResponse.redirected); - }); -}).timeout(10_000); - -describe('Header tests', () => { - it('Gets correct raw headers', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual( - removeDates(cachedFetchResponse.headers.raw()), - removeDates(standardFetchResponse.headers.raw()), - ); - - cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual( - removeDates(cachedFetchResponse.headers.raw()), - removeDates(standardFetchResponse.headers.raw()), - ); - }); - - it('Gets correct header keys', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual([...cachedFetchResponse.headers.keys()], [...standardFetchResponse.headers.keys()]); - - cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual([...cachedFetchResponse.headers.keys()], [...standardFetchResponse.headers.keys()]); - }); - - it('Gets correct header values', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual( - removeDates([...cachedFetchResponse.headers.values()]), - removeDates([...standardFetchResponse.headers.values()]), - ); - - cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual( - removeDates([...cachedFetchResponse.headers.values()]), - removeDates([...standardFetchResponse.headers.values()]), - ); - }); - - it('Gets correct header entries', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual( - removeDates([...cachedFetchResponse.headers.entries()]), - removeDates([...standardFetchResponse.headers.entries()]), - ); - - cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual( - removeDates([...cachedFetchResponse.headers.entries()]), - removeDates([...standardFetchResponse.headers.entries()]), - ); - }); - - it('Can get a header by value', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); - assert(standardFetchResponse.headers.get('content-length')); - assert.deepStrictEqual( - cachedFetchResponse.headers.get('content-length'), - standardFetchResponse.headers.get('content-length'), - ); - - cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual( - cachedFetchResponse.headers.get('content-length'), - standardFetchResponse.headers.get('content-length'), - ); - }); - - it('Returns undefined for non-existent header', async () => { - const headerName = 'zzzz'; - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); - assert(!standardFetchResponse.headers.get(headerName)); - assert.deepStrictEqual(cachedFetchResponse.headers.get(headerName), standardFetchResponse.headers.get(headerName)); - - cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual(cachedFetchResponse.headers.get(headerName), standardFetchResponse.headers.get(headerName)); - }); - - it('Can get whether a header is present', async () => { - let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); - assert(standardFetchResponse.headers.has('content-length')); - assert.deepStrictEqual( - cachedFetchResponse.headers.has('content-length'), - standardFetchResponse.headers.has('content-length'), - ); - - cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.deepStrictEqual( - cachedFetchResponse.headers.has('content-length'), - standardFetchResponse.headers.has('content-length'), - ); - }); -}).timeout(10_000); - -describe('Cache tests', () => { - it('Uses cache', async () => { - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Can eject from cache', async () => { - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - - await response.ejectFromCache(); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Does not error if ejecting from cache twice', async () => { - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - await response.ejectFromCache(); - await response.ejectFromCache(); - }); - - it('Gives different string bodies different cache keys', async () => { - response = await defaultCachedFetch(TWO_HUNDRED_URL, post('a')); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post('b')); - assert.strictEqual(response.returnedFromCache, false); - }); - - it('Gives same string bodies same cache keys', async () => { - response = await defaultCachedFetch(TWO_HUNDRED_URL, post('a')); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post('a')); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Gives different URLSearchParams different cache keys', async () => { - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(new URLSearchParams('a=a'))); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(new URLSearchParams('a=b'))); - assert.strictEqual(response.returnedFromCache, false); - }); - - it('Gives same URLSearchParams same cache keys', async () => { - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(new URLSearchParams('a=a'))); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(new URLSearchParams('a=a'))); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Gives different read streams different cache keys', async () => { - const s1 = fs.createReadStream(path.join(__dirname, 'expected_png.png')); - const s2 = fs.createReadStream(path.join(__dirname, '..', 'src', 'index.ts')); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(s1)); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(s2)); - assert.strictEqual(response.returnedFromCache, false); - }); - - it('Gives the same read streams the same cache key', async () => { - const s1 = fs.createReadStream(path.join(__dirname, 'expected_png.png')); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(s1)); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(s1)); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Gives different form data different cache keys', async () => { - const data1 = new FormData(); - data1.append('a', 'a'); - - const data2 = new FormData(); - data2.append('b', 'b'); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(data1)); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(data2)); - assert.strictEqual(response.returnedFromCache, false); - }); - - it('Gives same form data same cache keys', async () => { - const data1 = new FormData(); - data1.append('a', 'a'); - - const data2 = new FormData(); - data2.append('a', 'a'); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(data1)); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL, post(data2)); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Does not error with custom agent with circular properties', async () => { - const agent = new Agent(); - (agent as any).agent = agent; - - await defaultCachedFetch(TWO_HUNDRED_URL, { agent }); - }); - - it('Works with a TTL of 0', async () => { - const cachedFetch = FetchCache.create({ cache: new FileSystemCache({ ttl: 0 }) }); - - const response = await cachedFetch(TWO_HUNDRED_URL); - assert(response.ok); - }); - - it('Uses a shared global memory cache by default', async () => { - const cachedFetch1 = FetchCache.create({}); - const cachedFetch2 = FetchCache.create({}); - - assert.strictEqual(cachedFetch1.options.cache, cachedFetch2.options.cache); - - const response1 = await cachedFetch1(TWO_HUNDRED_URL); - const response2 = await cachedFetch2(TWO_HUNDRED_URL); - - assert.strictEqual(response1.returnedFromCache, false); - assert.strictEqual(response2.returnedFromCache, true); - }); - - it('Can use a client-provided custom cache key', async () => { - const cacheFunction = async (resource: FetchResource) => { - if (resource instanceof StandardFetchRequest) { - return resource.url; - } - - return resource.toString(); - }; - - const cachedFetch = FetchCache.create({ calculateCacheKey: cacheFunction }); - const response1 = await cachedFetch(TWO_HUNDRED_URL, { headers: { XXX: 'YYY' } }); - const response2 = await cachedFetch(TWO_HUNDRED_URL, { headers: { XXX: 'ZZZ' } }); - - assert.strictEqual(response1.returnedFromCache, false); - assert.strictEqual(response2.returnedFromCache, true); - - const response3 = await cachedFetch(FOUR_HUNDRED_URL, { headers: { XXX: 'YYY' } }); - assert.strictEqual(response3.returnedFromCache, false); - }); -}).timeout(10_000); - -describe('Data tests', () => { - it('Supports request objects', async () => { - let request = new StandardFetchRequest('https://google.com', { body: 'test', method: 'POST' }); - response = await defaultCachedFetch(request); - assert.strictEqual(response.returnedFromCache, false); - - request = new StandardFetchRequest('https://google.com', { body: 'test', method: 'POST' }); - response = await defaultCachedFetch(request); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Supports request objects with custom headers', async () => { - const request1 = new StandardFetchRequest(TWO_HUNDRED_URL, { headers: { XXX: 'YYY' } }); - const request2 = new StandardFetchRequest(TWO_HUNDRED_URL, { headers: { XXX: 'ZZZ' } }); - - response = await defaultCachedFetch(request1); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(request2); - assert.strictEqual(response.returnedFromCache, false); - }); - - it('Refuses to consume body twice', async () => { - response = await defaultCachedFetch(TEXT_BODY_URL); - await response.text(); - await assert.rejects(async () => response.text(), /body used already for:/); - }); - - it('Can get text body', async () => { - response = await defaultCachedFetch(TEXT_BODY_URL); - const body1 = await response.text(); - assert.strictEqual(body1, TEXT_BODY_EXPECTED); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TEXT_BODY_URL); - const body2 = await response.text(); - assert.strictEqual(body2, TEXT_BODY_EXPECTED); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Can get JSON body', async () => { - response = await defaultCachedFetch(JSON_BODY_URL); - const body1 = (await response.json()) as { slideshow: unknown }; - assert(body1?.slideshow); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(JSON_BODY_URL); - const body2 = (await response.json()) as { slideshow: unknown }; - assert(body2.slideshow); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Can get PNG buffer body', async () => { - response = await defaultCachedFetch(PNG_BODY_URL); - const body1 = await response.buffer(); - assert.strictEqual(expectedPngBuffer.equals(body1), true); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(PNG_BODY_URL); - const body2 = await response.buffer(); - assert.strictEqual(expectedPngBuffer.equals(body2), true); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Can stream a body', async () => { - response = await defaultCachedFetch(TEXT_BODY_URL); - let body = ''; - - for await (const chunk of response.body!) { - body += chunk.toString(); - } - - assert.strictEqual(TEXT_BODY_EXPECTED, body); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TEXT_BODY_URL); - body = ''; - - for await (const chunk of response.body!) { - body += chunk.toString(); - } - - assert.strictEqual(TEXT_BODY_EXPECTED, body); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Errors if the body type is not supported', async () => { - await assert.rejects( - async () => defaultCachedFetch(TEXT_BODY_URL, { body: 1 as unknown as string }), - /Unsupported body type/, - ); - }); - - it('Errors if the resource type is not supported', async () => { - await assert.rejects( - async () => defaultCachedFetch(1 as unknown as string), - /The first argument to fetch must be either a string or a node-fetch Request instance/, - ); - }); - - it('Uses cache even if you make multiple requests at the same time', async () => { - const [response1, response] = await Promise.all([ - defaultCachedFetch(TWO_HUNDRED_URL), - defaultCachedFetch(TWO_HUNDRED_URL), - ]); - - // One should be false, the other should be true - assert(response1.returnedFromCache !== response.returnedFromCache); - }); - - it('Allows a custom synchronization strategy', async () => { - const bogusSynchronizationStrategy: ISynchronizationStrategy = { - doWithExclusiveLock: async (_, action) => action(), - }; - - defaultCachedFetch = FetchCache.create({ - cache: new FileSystemCache(), - synchronizationStrategy: bogusSynchronizationStrategy, - }); - - const responses = await Promise.all( - Array(10) - .fill(0) - .map(async () => defaultCachedFetch(TWO_HUNDRED_URL)), - ); - - // Since our bogus synchronization strategy doesn't actually synchronize, - // at least two responses should be cache misses (this depends on random - // timing and might be a little flaky). - assert(responses.filter(response => !response.returnedFromCache).length > 1); - }); - - it('Can stream a hundred thousand bytes to a file in ten chunks', async () => { - defaultCachedFetch = FetchCache.create({ cache: new FileSystemCache() }); - - const initialResponse = await defaultCachedFetch(HUNDRED_THOUSAND_BYTES_URL); - assert(initialResponse.ok); - assert(!initialResponse.returnedFromCache); - - const initialResponseBuffer = await initialResponse.arrayBuffer(); - assert.equal(initialResponseBuffer.byteLength, 100_000); - - const secondResponse = await defaultCachedFetch(HUNDRED_THOUSAND_BYTES_URL); - assert(secondResponse.ok); - assert(secondResponse.returnedFromCache); - - const secondResponseBuffer = await secondResponse.arrayBuffer(); - assert.equal(secondResponseBuffer.byteLength, 100_000); - }); -}).timeout(10_000); - -describe('Memory cache tests', () => { - it('Supports TTL', async () => { - defaultCachedFetch = FetchCache.create({ cache: new MemoryCache({ ttl: 100 }) }); - let response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - - await wait(200); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - }); -}).timeout(10_000); - -describe('File system cache tests', () => { - it('Supports TTL', async () => { - defaultCachedFetch = FetchCache.create({ cache: new FileSystemCache({ ttl: 100 }) }); - let response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - - await wait(200); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - }); - - it('Can be cleared', async () => { - const cache = new FileSystemCache(); - defaultCachedFetch = FetchCache.create({ cache }); - let response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - - await cache.clear(); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - }); - - it('Can get PNG buffer body', async () => { - defaultCachedFetch = FetchCache.create({ cache: new FileSystemCache() }); - response = await defaultCachedFetch(PNG_BODY_URL); - const body1 = await response.buffer(); - assert.strictEqual(expectedPngBuffer.equals(body1), true); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(PNG_BODY_URL); - const body2 = await response.buffer(); - assert.strictEqual(expectedPngBuffer.equals(body2), true); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Can eject from cache', async () => { - defaultCachedFetch = FetchCache.create({ cache: new FileSystemCache() }); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - - await response.ejectFromCache(); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - }); -}); - -describe('Cache mode tests', () => { - it('Can use the only-if-cached cache control setting via init', async () => { - response = await defaultCachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }); - assert(response.status === 504 && response.isCacheMiss); - response = await defaultCachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }); - assert(response.status === 504 && response.isCacheMiss); - response = await defaultCachedFetch(TWO_HUNDRED_URL); - assert(response && !response.returnedFromCache); - response = await defaultCachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }); - assert(response?.returnedFromCache); - await response.ejectFromCache(); - response = await defaultCachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }); - assert(response.status === 504 && response.isCacheMiss); - }); - - it('Can use the only-if-cached cache control setting via resource', async () => { - response = await defaultCachedFetch( - new StandardFetchRequest(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }), - ); - assert(response.status === 504 && response.isCacheMiss); - response = await defaultCachedFetch(new StandardFetchRequest(TWO_HUNDRED_URL)); - assert(response && !response.returnedFromCache); - response = await defaultCachedFetch( - new StandardFetchRequest(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }), - ); - assert(response?.returnedFromCache); - }); - - it('Works with only-if-cached along with other cache-control directives', async () => { - response = await defaultCachedFetch( - new StandardFetchRequest(TWO_HUNDRED_URL, { headers: { 'cAcHe-cOnTrOl': ' only-if-cached , no-store ' } }), - ); - assert(response.status === 504 && response.isCacheMiss); - response = await defaultCachedFetch(TWO_HUNDRED_URL, { - headers: { 'cAcHe-cOnTrOl': ' only-if-cached , no-store ' }, - }); - assert(response.status === 504 && response.isCacheMiss); - }); -}); - -describe('Cache key tests', () => { - it('Can calculate a cache key and check that it exists', async () => { - await defaultCachedFetch(TWO_HUNDRED_URL); - - const cacheKey = await calculateCacheKey(TWO_HUNDRED_URL); - const nonExistentCacheKey = await calculateCacheKey(TEXT_BODY_URL); - - const cacheKeyResult = await defaultCache.get(cacheKey); - const nonExistentCacheKeyResult = await defaultCache.get(nonExistentCacheKey); - - assert(cacheKeyResult); - assert(!nonExistentCacheKeyResult); - }); -}); - -describe('Cache strategy tests', () => { - it('Can use a custom cache strategy to cache only OKAY responses', async () => { - const customCachedFetch = FetchCache.create({ - cache: defaultCache, - shouldCacheResponse: cacheStrategies.cacheOkayOnly, - }); - - response = await customCachedFetch(FOUR_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - response = await customCachedFetch(FOUR_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - response = await customCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - response = await customCachedFetch(TWO_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Can use a custom cache strategy via the call to fetch to cache only OKAY responses', async () => { - const customCachedFetch = FetchCache.create({ - cache: defaultCache, - }); - - response = await customCachedFetch(FOUR_HUNDRED_URL, undefined, { - shouldCacheResponse: cacheStrategies.cacheOkayOnly, - }); - assert.strictEqual(response.returnedFromCache, false); - - response = await customCachedFetch(FOUR_HUNDRED_URL, undefined, { - shouldCacheResponse: cacheStrategies.cacheOkayOnly, - }); - assert.strictEqual(response.returnedFromCache, false); - - response = await customCachedFetch(FOUR_HUNDRED_URL, undefined, { - shouldCacheResponse: cacheStrategies.cacheNon5xxOnly, - }); - assert.strictEqual(response.returnedFromCache, false); - - response = await customCachedFetch(FOUR_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - }); - - it('Can use the cacheNon5xxOnly built-in strategy', async () => { - const customCachedFetch = FetchCache.create({ - cache: defaultCache, - shouldCacheResponse: cacheStrategies.cacheNon5xxOnly, - }); - - response = await customCachedFetch(FOUR_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - response = await customCachedFetch(FOUR_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, true); - - response = await customCachedFetch(FIVE_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - - response = await customCachedFetch(FIVE_HUNDRED_URL); - assert.strictEqual(response.returnedFromCache, false); - }); - - it('Can use a custom cache strategy that uses the response body', async () => { - const customCachedFetch = FetchCache.create({ - cache: defaultCache, - async shouldCacheResponse(response) { - const body = await response.text(); - return Boolean(body); - }, - }); - - response = await customCachedFetch(TEXT_BODY_URL); - assert.strictEqual(response.returnedFromCache, false); - assert.strictEqual(await response.text(), TEXT_BODY_EXPECTED); - }); - - it('Can use a custom cache strategy that uses the response for all response types', async () => { - const functionsThatUseResponse = [ - 'arrayBuffer', - 'blob', - 'buffer', - 'json', - 'text', - ] as const; - - await Promise.all( - functionsThatUseResponse.map(async functionName => { - const newFetch = FetchCache.create({ - cache: new MemoryCache(), - }); - - response = await newFetch(JSON_BODY_URL, undefined, { - async shouldCacheResponse(response) { - await response[functionName](); - return true; - }, - }); - - assert.strictEqual(response.returnedFromCache, false); - - // Because when the json() function is used all of the whitespace in - // the response is lost, something a little special happens when we - // snipe the response body from json(). The cached response will have - // the whitespace stripped out, even though the original response may - // not have. This may cause issues for some unusual use cases. - if (functionName === 'json') { - assert.strictEqual(await response.text(), JSON.stringify(JSON.parse(JSON_BODY_EXPECTED))); - } else { - assert.strictEqual(await response.text(), JSON_BODY_EXPECTED); - } - }), - ); - }); -}); - -describe('Network error tests', () => { - it('Bubbles up network errors', async () => { - await assert.rejects(async () => defaultCachedFetch('http://localhost:1'), /^FetchError:/); - }); -}); From 622be2151e9addc7a96e346c6f5ad23bd758755d Mon Sep 17 00:00:00 2001 From: mistval Date: Thu, 9 Oct 2025 18:48:20 -0700 Subject: [PATCH 3/5] wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e2c3e4..1d0526b 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Options: } ``` -If you set a TTL, be aware that cache entries are not actively deleted from disk when they become invalid, which can cause disk bloat over time. To clean that up, you can clear the entire cache directory by calling `.clear()` on an instance of `FileSystemCache`. +If you set a TTL, be aware that cache entries are not actively deleted from disk when they become invalid, which can cause disk bloat over time. To clean that up, you can periodically clear the entire cache directory by calling `.clear()` on an instance of `FileSystemCache`. ### Cache with Redis From 8e9757ac81a2daaa3c19c2a932dda5ef839d1f3e Mon Sep 17 00:00:00 2001 From: mistval Date: Thu, 9 Oct 2025 18:49:09 -0700 Subject: [PATCH 4/5] wording --- test/tests.ts | 789 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 789 insertions(+) create mode 100644 test/tests.ts diff --git a/test/tests.ts b/test/tests.ts new file mode 100644 index 0000000..b07a1f0 --- /dev/null +++ b/test/tests.ts @@ -0,0 +1,789 @@ +// eslint-disable-next-line import/no-unassigned-import,import/order +import 'dotenv/config.js'; +import path, { dirname } from 'path'; +import util from 'util'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; +import assert from 'assert'; +import { Agent } from 'http'; +import { rimraf } from 'rimraf'; +import { FormData } from 'formdata-node'; +import standardFetch, { Request as StandardFetchRequest } from 'node-fetch'; +import FetchCache, { + MemoryCache, + FileSystemCache, + cacheStrategies, + FetchResource, + NFCResponse, + calculateCacheKey, + ISynchronizationStrategy, +} from '../src/index.js'; + +const httpBinBaseUrl = 'http://localhost:3000'; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const wait = util.promisify(setTimeout); + +const CACHE_PATH = path.join(__dirname, '..', '.cache'); +const expectedPngBuffer = fs.readFileSync(path.join(__dirname, 'expected_png.png')); + +const TWO_HUNDRED_URL = `${httpBinBaseUrl}/status/200`; +const FOUR_HUNDRED_URL = `${httpBinBaseUrl}/status/400`; +const FIVE_HUNDRED_URL = `${httpBinBaseUrl}/status/500`; +const THREE_HUNDRED_TWO_URL = `${httpBinBaseUrl}/status/302`; +const TEXT_BODY_URL = `${httpBinBaseUrl}/robots.txt`; +const JSON_BODY_URL = `${httpBinBaseUrl}/json`; +const PNG_BODY_URL = `${httpBinBaseUrl}/image/png`; +const HUNDRED_THOUSAND_BYTES_URL = `${httpBinBaseUrl}/stream-bytes/100000?chunk_size=10000`; + +const TEXT_BODY_EXPECTED = 'User-agent: *\nDisallow: /deny\n'; +const JSON_BODY_EXPECTED = `{ + "slideshow": { + "author": "Yours Truly", + "date": "date of publication", + "slides": [ + { + "title": "Wake up to WonderWidgets!", + "type": "all" + }, + { + "items": [ + "Why WonderWidgets are great", + "Who buys WonderWidgets" + ], + "title": "Overview", + "type": "all" + } + ], + "title": "Sample Slide Show" + } +} +`; + +let defaultCachedFetch: typeof FetchCache; +let defaultCache: MemoryCache; + +function post(body: string | URLSearchParams | FormData | fs.ReadStream) { + return { method: 'POST', body }; +} + +function removeDates(arrayOrObject: { date?: unknown } | string[] | string[][]) { + if (Array.isArray(arrayOrObject)) { + if (Array.isArray(arrayOrObject[0])) { + return arrayOrObject.filter(element => element[0] !== 'date'); + } + + return (arrayOrObject as string[]).filter(element => !Date.parse(element)); + } + + if (arrayOrObject.date) { + const copy = { ...arrayOrObject }; + delete copy.date; + return copy; + } + + return arrayOrObject; +} + +async function dualFetch(...args: Parameters) { + const [cachedFetchResponse, standardFetchResponse] = await Promise.all([ + defaultCachedFetch(...args), + standardFetch(...args), + ]); + + return { cachedFetchResponse, standardFetchResponse }; +} + +beforeEach(async () => { + rimraf.sync(CACHE_PATH); + defaultCache = new MemoryCache(); + defaultCachedFetch = FetchCache.create({ cache: defaultCache }); +}); + +let response: NFCResponse; + +describe('Basic property tests', () => { + it('Has a status property', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); + assert.strictEqual(cachedFetchResponse.status, standardFetchResponse.status); + + cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(cachedFetchResponse.status, standardFetchResponse.status); + }); + + it('Has a statusText property', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); + assert.strictEqual(cachedFetchResponse.statusText, standardFetchResponse.statusText); + + cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(cachedFetchResponse.statusText, standardFetchResponse.statusText); + }); + + it('Has a url property', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); + assert.strictEqual(cachedFetchResponse.url, standardFetchResponse.url); + + cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(cachedFetchResponse.url, standardFetchResponse.url); + }); + + it('Has an ok property', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(FOUR_HUNDRED_URL); + assert.strictEqual(cachedFetchResponse.ok, standardFetchResponse.ok); + assert.strictEqual(cachedFetchResponse.status, standardFetchResponse.status); + + cachedFetchResponse = await defaultCachedFetch(FOUR_HUNDRED_URL); + assert.strictEqual(cachedFetchResponse.ok, standardFetchResponse.ok); + assert.strictEqual(cachedFetchResponse.status, standardFetchResponse.status); + }); + + it('Has a redirected property', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(THREE_HUNDRED_TWO_URL); + assert.strictEqual(cachedFetchResponse.redirected, standardFetchResponse.redirected); + + cachedFetchResponse = await defaultCachedFetch(THREE_HUNDRED_TWO_URL); + assert.strictEqual(cachedFetchResponse.redirected, standardFetchResponse.redirected); + }); +}).timeout(10_000); + +describe('Header tests', () => { + it('Gets correct raw headers', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual( + removeDates(cachedFetchResponse.headers.raw()), + removeDates(standardFetchResponse.headers.raw()), + ); + + cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual( + removeDates(cachedFetchResponse.headers.raw()), + removeDates(standardFetchResponse.headers.raw()), + ); + }); + + it('Gets correct header keys', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual([...cachedFetchResponse.headers.keys()], [...standardFetchResponse.headers.keys()]); + + cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual([...cachedFetchResponse.headers.keys()], [...standardFetchResponse.headers.keys()]); + }); + + it('Gets correct header values', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual( + removeDates([...cachedFetchResponse.headers.values()]), + removeDates([...standardFetchResponse.headers.values()]), + ); + + cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual( + removeDates([...cachedFetchResponse.headers.values()]), + removeDates([...standardFetchResponse.headers.values()]), + ); + }); + + it('Gets correct header entries', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual( + removeDates([...cachedFetchResponse.headers.entries()]), + removeDates([...standardFetchResponse.headers.entries()]), + ); + + cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual( + removeDates([...cachedFetchResponse.headers.entries()]), + removeDates([...standardFetchResponse.headers.entries()]), + ); + }); + + it('Can get a header by value', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); + assert(standardFetchResponse.headers.get('content-length')); + assert.deepStrictEqual( + cachedFetchResponse.headers.get('content-length'), + standardFetchResponse.headers.get('content-length'), + ); + + cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual( + cachedFetchResponse.headers.get('content-length'), + standardFetchResponse.headers.get('content-length'), + ); + }); + + it('Returns undefined for non-existent header', async () => { + const headerName = 'zzzz'; + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); + assert(!standardFetchResponse.headers.get(headerName)); + assert.deepStrictEqual(cachedFetchResponse.headers.get(headerName), standardFetchResponse.headers.get(headerName)); + + cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual(cachedFetchResponse.headers.get(headerName), standardFetchResponse.headers.get(headerName)); + }); + + it('Can get whether a header is present', async () => { + let { cachedFetchResponse, standardFetchResponse } = await dualFetch(TWO_HUNDRED_URL); + assert(standardFetchResponse.headers.has('content-length')); + assert.deepStrictEqual( + cachedFetchResponse.headers.has('content-length'), + standardFetchResponse.headers.has('content-length'), + ); + + cachedFetchResponse = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.deepStrictEqual( + cachedFetchResponse.headers.has('content-length'), + standardFetchResponse.headers.has('content-length'), + ); + }); +}).timeout(10_000); + +describe('Cache tests', () => { + it('Uses cache', async () => { + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Can eject from cache', async () => { + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + + await response.ejectFromCache(); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Does not error if ejecting from cache twice', async () => { + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + await response.ejectFromCache(); + await response.ejectFromCache(); + }); + + it('Gives different string bodies different cache keys', async () => { + response = await defaultCachedFetch(TWO_HUNDRED_URL, post('a')); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post('b')); + assert.strictEqual(response.returnedFromCache, false); + }); + + it('Gives same string bodies same cache keys', async () => { + response = await defaultCachedFetch(TWO_HUNDRED_URL, post('a')); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post('a')); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Gives different URLSearchParams different cache keys', async () => { + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(new URLSearchParams('a=a'))); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(new URLSearchParams('a=b'))); + assert.strictEqual(response.returnedFromCache, false); + }); + + it('Gives same URLSearchParams same cache keys', async () => { + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(new URLSearchParams('a=a'))); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(new URLSearchParams('a=a'))); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Gives different read streams different cache keys', async () => { + const s1 = fs.createReadStream(path.join(__dirname, 'expected_png.png')); + const s2 = fs.createReadStream(path.join(__dirname, '..', 'src', 'index.ts')); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(s1)); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(s2)); + assert.strictEqual(response.returnedFromCache, false); + }); + + it('Gives the same read streams the same cache key', async () => { + const s1 = fs.createReadStream(path.join(__dirname, 'expected_png.png')); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(s1)); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(s1)); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Gives different form data different cache keys', async () => { + const data1 = new FormData(); + data1.append('a', 'a'); + + const data2 = new FormData(); + data2.append('b', 'b'); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(data1)); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(data2)); + assert.strictEqual(response.returnedFromCache, false); + }); + + it('Gives same form data same cache keys', async () => { + const data1 = new FormData(); + data1.append('a', 'a'); + + const data2 = new FormData(); + data2.append('a', 'a'); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(data1)); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL, post(data2)); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Does not error with custom agent with circular properties', async () => { + const agent = new Agent(); + (agent as any).agent = agent; + + await defaultCachedFetch(TWO_HUNDRED_URL, { agent }); + }); + + it('Works with a TTL of 0', async () => { + const cachedFetch = FetchCache.create({ cache: new FileSystemCache({ ttl: 0 }) }); + + const response = await cachedFetch(TWO_HUNDRED_URL); + assert(response.ok); + }); + + it('Uses a shared global memory cache by default', async () => { + const cachedFetch1 = FetchCache.create({}); + const cachedFetch2 = FetchCache.create({}); + + assert.strictEqual(cachedFetch1.options.cache, cachedFetch2.options.cache); + + const response1 = await cachedFetch1(TWO_HUNDRED_URL); + const response2 = await cachedFetch2(TWO_HUNDRED_URL); + + assert.strictEqual(response1.returnedFromCache, false); + assert.strictEqual(response2.returnedFromCache, true); + }); + + it('Can use a client-provided custom cache key', async () => { + const cacheFunction = async (resource: FetchResource) => { + if (resource instanceof StandardFetchRequest) { + return resource.url; + } + + return resource.toString(); + }; + + const cachedFetch = FetchCache.create({ calculateCacheKey: cacheFunction }); + const response1 = await cachedFetch(TWO_HUNDRED_URL, { headers: { XXX: 'YYY' } }); + const response2 = await cachedFetch(TWO_HUNDRED_URL, { headers: { XXX: 'ZZZ' } }); + + assert.strictEqual(response1.returnedFromCache, false); + assert.strictEqual(response2.returnedFromCache, true); + + const response3 = await cachedFetch(FOUR_HUNDRED_URL, { headers: { XXX: 'YYY' } }); + assert.strictEqual(response3.returnedFromCache, false); + }); +}).timeout(10_000); + +describe('Data tests', () => { + it('Supports request objects', async () => { + let request = new StandardFetchRequest('https://google.com', { body: 'test', method: 'POST' }); + response = await defaultCachedFetch(request); + assert.strictEqual(response.returnedFromCache, false); + + request = new StandardFetchRequest('https://google.com', { body: 'test', method: 'POST' }); + response = await defaultCachedFetch(request); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Supports request objects with custom headers', async () => { + const request1 = new StandardFetchRequest(TWO_HUNDRED_URL, { headers: { XXX: 'YYY' } }); + const request2 = new StandardFetchRequest(TWO_HUNDRED_URL, { headers: { XXX: 'ZZZ' } }); + + response = await defaultCachedFetch(request1); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(request2); + assert.strictEqual(response.returnedFromCache, false); + }); + + it('Refuses to consume body twice', async () => { + response = await defaultCachedFetch(TEXT_BODY_URL); + await response.text(); + await assert.rejects(async () => response.text(), /body used already for:/); + }); + + it('Can get text body', async () => { + response = await defaultCachedFetch(TEXT_BODY_URL); + const body1 = await response.text(); + assert.strictEqual(body1, TEXT_BODY_EXPECTED); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TEXT_BODY_URL); + const body2 = await response.text(); + assert.strictEqual(body2, TEXT_BODY_EXPECTED); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Can get JSON body', async () => { + response = await defaultCachedFetch(JSON_BODY_URL); + const body1 = (await response.json()) as { slideshow: unknown }; + assert(body1?.slideshow); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(JSON_BODY_URL); + const body2 = (await response.json()) as { slideshow: unknown }; + assert(body2.slideshow); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Can get PNG buffer body', async () => { + response = await defaultCachedFetch(PNG_BODY_URL); + const body1 = await response.buffer(); + assert.strictEqual(expectedPngBuffer.equals(body1), true); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(PNG_BODY_URL); + const body2 = await response.buffer(); + assert.strictEqual(expectedPngBuffer.equals(body2), true); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Can stream a body', async () => { + response = await defaultCachedFetch(TEXT_BODY_URL); + let body = ''; + + for await (const chunk of response.body!) { + body += chunk.toString(); + } + + assert.strictEqual(TEXT_BODY_EXPECTED, body); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TEXT_BODY_URL); + body = ''; + + for await (const chunk of response.body!) { + body += chunk.toString(); + } + + assert.strictEqual(TEXT_BODY_EXPECTED, body); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Errors if the body type is not supported', async () => { + await assert.rejects( + async () => defaultCachedFetch(TEXT_BODY_URL, { body: 1 as unknown as string }), + /Unsupported body type/, + ); + }); + + it('Errors if the resource type is not supported', async () => { + await assert.rejects( + async () => defaultCachedFetch(1 as unknown as string), + /The first argument to fetch must be either a string or a node-fetch Request instance/, + ); + }); + + it('Uses cache even if you make multiple requests at the same time', async () => { + const [response1, response] = await Promise.all([ + defaultCachedFetch(TWO_HUNDRED_URL), + defaultCachedFetch(TWO_HUNDRED_URL), + ]); + + // One should be false, the other should be true + assert(response1.returnedFromCache !== response.returnedFromCache); + }); + + it('Allows a custom synchronization strategy', async () => { + const bogusSynchronizationStrategy: ISynchronizationStrategy = { + doWithExclusiveLock: async (_, action) => action(), + }; + + defaultCachedFetch = FetchCache.create({ + cache: new FileSystemCache(), + synchronizationStrategy: bogusSynchronizationStrategy, + }); + + const responses = await Promise.all( + Array(10) + .fill(0) + .map(async () => defaultCachedFetch(TWO_HUNDRED_URL)), + ); + + // Since our bogus synchronization strategy doesn't actually synchronize, + // at least two responses should be cache misses (this depends on random + // timing and might be a little flaky). + assert(responses.filter(response => !response.returnedFromCache).length > 1); + }); + + it('Can stream a hundred thousand bytes to a file in ten chunks', async () => { + defaultCachedFetch = FetchCache.create({ cache: new FileSystemCache() }); + + const initialResponse = await defaultCachedFetch(HUNDRED_THOUSAND_BYTES_URL); + assert(initialResponse.ok); + assert(!initialResponse.returnedFromCache); + + const initialResponseBuffer = await initialResponse.arrayBuffer(); + assert.equal(initialResponseBuffer.byteLength, 100_000); + + const secondResponse = await defaultCachedFetch(HUNDRED_THOUSAND_BYTES_URL); + assert(secondResponse.ok); + assert(secondResponse.returnedFromCache); + + const secondResponseBuffer = await secondResponse.arrayBuffer(); + assert.equal(secondResponseBuffer.byteLength, 100_000); + }); +}).timeout(10_000); + +describe('Memory cache tests', () => { + it('Supports TTL', async () => { + defaultCachedFetch = FetchCache.create({ cache: new MemoryCache({ ttl: 100 }) }); + let response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + + await wait(200); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + }); +}).timeout(10_000); + +describe('File system cache tests', () => { + it('Supports TTL', async () => { + defaultCachedFetch = FetchCache.create({ cache: new FileSystemCache({ ttl: 100 }) }); + let response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + + await wait(200); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + }); + + it('Can get PNG buffer body', async () => { + defaultCachedFetch = FetchCache.create({ cache: new FileSystemCache() }); + response = await defaultCachedFetch(PNG_BODY_URL); + const body1 = await response.buffer(); + assert.strictEqual(expectedPngBuffer.equals(body1), true); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(PNG_BODY_URL); + const body2 = await response.buffer(); + assert.strictEqual(expectedPngBuffer.equals(body2), true); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Can eject from cache', async () => { + defaultCachedFetch = FetchCache.create({ cache: new FileSystemCache() }); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + + await response.ejectFromCache(); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + }); +}); + +describe('Cache mode tests', () => { + it('Can use the only-if-cached cache control setting via init', async () => { + response = await defaultCachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }); + assert(response.status === 504 && response.isCacheMiss); + response = await defaultCachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }); + assert(response.status === 504 && response.isCacheMiss); + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert(response && !response.returnedFromCache); + response = await defaultCachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }); + assert(response?.returnedFromCache); + await response.ejectFromCache(); + response = await defaultCachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }); + assert(response.status === 504 && response.isCacheMiss); + }); + + it('Can use the only-if-cached cache control setting via resource', async () => { + response = await defaultCachedFetch( + new StandardFetchRequest(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }), + ); + assert(response.status === 504 && response.isCacheMiss); + response = await defaultCachedFetch(new StandardFetchRequest(TWO_HUNDRED_URL)); + assert(response && !response.returnedFromCache); + response = await defaultCachedFetch( + new StandardFetchRequest(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }), + ); + assert(response?.returnedFromCache); + }); + + it('Works with only-if-cached along with other cache-control directives', async () => { + response = await defaultCachedFetch( + new StandardFetchRequest(TWO_HUNDRED_URL, { headers: { 'cAcHe-cOnTrOl': ' only-if-cached , no-store ' } }), + ); + assert(response.status === 504 && response.isCacheMiss); + response = await defaultCachedFetch(TWO_HUNDRED_URL, { + headers: { 'cAcHe-cOnTrOl': ' only-if-cached , no-store ' }, + }); + assert(response.status === 504 && response.isCacheMiss); + }); +}); + +describe('Cache key tests', () => { + it('Can calculate a cache key and check that it exists', async () => { + await defaultCachedFetch(TWO_HUNDRED_URL); + + const cacheKey = await calculateCacheKey(TWO_HUNDRED_URL); + const nonExistentCacheKey = await calculateCacheKey(TEXT_BODY_URL); + + const cacheKeyResult = await defaultCache.get(cacheKey); + const nonExistentCacheKeyResult = await defaultCache.get(nonExistentCacheKey); + + assert(cacheKeyResult); + assert(!nonExistentCacheKeyResult); + }); +}); + +describe('Cache strategy tests', () => { + it('Can use a custom cache strategy to cache only OKAY responses', async () => { + const customCachedFetch = FetchCache.create({ + cache: defaultCache, + shouldCacheResponse: cacheStrategies.cacheOkayOnly, + }); + + response = await customCachedFetch(FOUR_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + response = await customCachedFetch(FOUR_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + response = await customCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + response = await customCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Can use a custom cache strategy via the call to fetch to cache only OKAY responses', async () => { + const customCachedFetch = FetchCache.create({ + cache: defaultCache, + }); + + response = await customCachedFetch(FOUR_HUNDRED_URL, undefined, { + shouldCacheResponse: cacheStrategies.cacheOkayOnly, + }); + assert.strictEqual(response.returnedFromCache, false); + + response = await customCachedFetch(FOUR_HUNDRED_URL, undefined, { + shouldCacheResponse: cacheStrategies.cacheOkayOnly, + }); + assert.strictEqual(response.returnedFromCache, false); + + response = await customCachedFetch(FOUR_HUNDRED_URL, undefined, { + shouldCacheResponse: cacheStrategies.cacheNon5xxOnly, + }); + assert.strictEqual(response.returnedFromCache, false); + + response = await customCachedFetch(FOUR_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + }); + + it('Can use the cacheNon5xxOnly built-in strategy', async () => { + const customCachedFetch = FetchCache.create({ + cache: defaultCache, + shouldCacheResponse: cacheStrategies.cacheNon5xxOnly, + }); + + response = await customCachedFetch(FOUR_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + response = await customCachedFetch(FOUR_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + + response = await customCachedFetch(FIVE_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + + response = await customCachedFetch(FIVE_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + }); + + it('Can use a custom cache strategy that uses the response body', async () => { + const customCachedFetch = FetchCache.create({ + cache: defaultCache, + async shouldCacheResponse(response) { + const body = await response.text(); + return Boolean(body); + }, + }); + + response = await customCachedFetch(TEXT_BODY_URL); + assert.strictEqual(response.returnedFromCache, false); + assert.strictEqual(await response.text(), TEXT_BODY_EXPECTED); + }); + + it('Can use a custom cache strategy that uses the response for all response types', async () => { + const functionsThatUseResponse = [ + 'arrayBuffer', + 'blob', + 'buffer', + 'json', + 'text', + ] as const; + + await Promise.all( + functionsThatUseResponse.map(async functionName => { + const newFetch = FetchCache.create({ + cache: new MemoryCache(), + }); + + response = await newFetch(JSON_BODY_URL, undefined, { + async shouldCacheResponse(response) { + await response[functionName](); + return true; + }, + }); + + assert.strictEqual(response.returnedFromCache, false); + + // Because when the json() function is used all of the whitespace in + // the response is lost, something a little special happens when we + // snipe the response body from json(). The cached response will have + // the whitespace stripped out, even though the original response may + // not have. This may cause issues for some unusual use cases. + if (functionName === 'json') { + assert.strictEqual(await response.text(), JSON.stringify(JSON.parse(JSON_BODY_EXPECTED))); + } else { + assert.strictEqual(await response.text(), JSON_BODY_EXPECTED); + } + }), + ); + }); +}); + +describe('Network error tests', () => { + it('Bubbles up network errors', async () => { + await assert.rejects(async () => defaultCachedFetch('http://localhost:1'), /^FetchError:/); + }); +}); From 2bba1f5f1426e01bbc42148510f8d114d5ae0076 Mon Sep 17 00:00:00 2001 From: mistval Date: Thu, 9 Oct 2025 18:55:08 -0700 Subject: [PATCH 5/5] add test --- test/tests.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/tests.ts b/test/tests.ts index b07a1f0..abbbd99 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -579,6 +579,21 @@ describe('File system cache tests', () => { assert.strictEqual(response.returnedFromCache, false); }); + it('Can be cleared', async () => { + const cache = new FileSystemCache({ ttl: 100 }); + + defaultCachedFetch = FetchCache.create({ cache }); + let response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, true); + + await cache.clear(); + + response = await defaultCachedFetch(TWO_HUNDRED_URL); + assert.strictEqual(response.returnedFromCache, false); + }); + it('Can get PNG buffer body', async () => { defaultCachedFetch = FetchCache.create({ cache: new FileSystemCache() }); response = await defaultCachedFetch(PNG_BODY_URL);