40 Commits

Author SHA1 Message Date
0e0925d4a3 chore(deps): add renovate.json
Some checks failed
CI Pipeline / build (20.x) (push) Failing after 33s
CI Pipeline / build (20.x) (pull_request) Failing after 32s
2023-10-19 11:03:57 +00:00
360dc102bb ee
All checks were successful
CI Pipeline / build (20.x) (push) Successful in 36s
2023-10-19 12:00:47 +01:00
0c643e9b87 e
Some checks failed
CI Pipeline / build (20.x) (push) Failing after 29s
2023-10-19 11:59:09 +01:00
a9f7d70cd0 fix package name
Some checks failed
CI Pipeline / build (20.x) (push) Failing after 32s
2023-10-19 11:57:40 +01:00
07777a74f2 ci stuff
Some checks failed
CI Pipeline / build (20.x) (push) Failing after 36s
2023-10-19 11:56:09 +01:00
d5382860f3 fix ci
Some checks failed
CI Pipeline / build (20.x) (push) Has been cancelled
2023-10-19 11:55:49 +01:00
73601127b2 7
Some checks failed
CI Pipeline / build (20.x) (push) Failing after 51s
2023-10-19 11:54:02 +01:00
eb86e3e4c5 gitea workflow
Some checks failed
CI Pipeline / build (20.x) (push) Failing after 27s
2023-10-19 11:52:43 +01:00
Randall
51e15ad046 Stop testing on Node v16, start testing on Node v21 2023-10-17 19:00:13 -04:00
dependabot[bot]
571b65cdd2 Bump @babel/traverse from 7.14.5 to 7.23.2
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.14.5 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-17 18:57:45 -04:00
dependabot[bot]
b0348460a7 Bump word-wrap from 1.2.3 to 1.2.4
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-18 19:01:50 -04:00
mistval
44a0ffb8f3 Update node-fetch to 2.6.11 2023-05-20 12:57:02 -04:00
Randall
41d52f39c8 Stop testing on Node 12 and 19, start testing on 16 and 20. 2023-05-12 23:22:59 -04:00
dependabot[bot]
3b92b39430 Bump yaml and husky
Removes [yaml](https://github.com/eemeli/yaml). It's no longer used after updating ancestor dependency [husky](https://github.com/typicode/husky). These dependencies need to be updated together.


Removes `yaml`

Updates `husky` from 4.3.8 to 8.0.3
- [Release notes](https://github.com/typicode/husky/releases)
- [Commits](https://github.com/typicode/husky/compare/v4.3.8...v8.0.3)

---
updated-dependencies:
- dependency-name: yaml
  dependency-type: indirect
- dependency-name: husky
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-24 19:42:26 -04:00
mistval
b234de4b69 Update dependency version to get fix for memory leak 2023-04-07 19:19:39 -04:00
mistval
44786dfb68 readme formatting 2023-03-13 21:00:02 -04:00
mistval
5f48f0fc8a add support for only-if-cached and expose cache key calculation function 2023-03-13 20:59:09 -04:00
mistval
4f93c9ba1c test on different node versions 2023-03-13 20:11:09 -04:00
mistval
faf73e49d8 test on different node versions 2023-03-13 20:09:41 -04:00
mistval
0173de3f6d update CI pipeline to Node 18 2023-01-07 10:27:03 -05:00
dependabot[bot]
decb904331 Bump minimatch from 3.0.4 to 3.1.2
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-07 10:23:57 -05:00
dependabot[bot]
7f502a98a2 Bump json5 from 1.0.1 to 1.0.2
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-07 10:22:41 -05:00
dependabot[bot]
18b276c5dd Bump semver-regex from 3.1.3 to 3.1.4
Bumps [semver-regex](https://github.com/sindresorhus/semver-regex) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/sindresorhus/semver-regex/releases)
- [Commits](https://github.com/sindresorhus/semver-regex/commits/v3.1.4)

---
updated-dependencies:
- dependency-name: semver-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-03 23:40:31 -04:00
dependabot[bot]
cd6f4f01e3 Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-26 09:12:28 -04:00
mistval
fee0cdd085 remove url import 2022-02-21 10:37:23 -05:00
mistval
2bc4cd6de0 update version 2022-02-19 08:55:21 -05:00
mistval
51e5754c3e update eslint 2022-02-19 08:54:14 -05:00
mistval
29de8ee068 update dependencies 2022-02-19 08:51:56 -05:00
dependabot[bot]
f1128f2f64 Bump node-fetch from 2.6.1 to 2.6.7 (#19)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-19 08:49:08 -05:00
mistval
0397d759dd update mocha 2022-02-19 08:48:33 -05:00
mistval
a68dd5b0d0 npm audit fix 2022-02-19 08:46:34 -05:00
dependabot[bot]
ba8ff5d257 Bump ajv from 6.12.0 to 6.12.6 (#18)
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.0 to 6.12.6.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.0...v6.12.6)

---
updated-dependencies:
- dependency-name: ajv
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-19 08:44:53 -05:00
dependabot[bot]
9c40ddf786 Bump tar from 6.1.5 to 6.1.11 (#14)
Bumps [tar](https://github.com/npm/node-tar) from 6.1.5 to 6.1.11.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.5...v6.1.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-31 20:34:16 -04:00
Randall Schmidt
9cea80be47 fix typo in readme 2021-08-19 15:09:44 -04:00
dependabot[bot]
a386a54fbb Bump path-parse from 1.0.6 to 1.0.7 (#13)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-12 08:55:50 -04:00
Randall Schmidt
b7190b933a bump version 2021-08-07 17:32:46 -04:00
Randall Schmidt
55b92ee40f have FileSystemCache TTLs survive the process exiting 2021-08-07 17:32:13 -04:00
dependabot[bot]
6607a71af6 Bump tar from 6.1.0 to 6.1.5 (#12)
Bumps [tar](https://github.com/npm/node-tar) from 6.1.0 to 6.1.5.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.0...v6.1.5)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-04 09:03:10 -04:00
Randall Schmidt
0f5f1edb22 switch to ESM examples 2021-07-11 09:11:26 -04:00
Randall Schmidt
d6f73c4778 update description 2021-07-11 09:08:46 -04:00
9 changed files with 5559 additions and 1742 deletions

23
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,23 @@
name: CI Pipeline
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-22.04
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,17 +0,0 @@
name: CI Pipeline
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- run: npm ci
- run: npm run lint
- run: npm run coverage

View File

@ -1,5 +1,7 @@
# node-fetch-cache # node-fetch-cache
Forked from: <https://github.com/mistval/node-fetch-cache>
node-fetch with caching of responses. node-fetch with caching of responses.
The first fetch with any given arguments will result in an HTTP request and any subsequent fetch with the same arguments will read the response from the cache. The first fetch with any given arguments will result in an HTTP request and any subsequent fetch with the same arguments will read the response from the cache.
@ -11,11 +13,11 @@ By default responses are cached in memory, but you can also cache to files on di
Require it and use it the same way you would use node-fetch: Require it and use it the same way you would use node-fetch:
```js ```js
const fetch = require('node-fetch-cache'); import fetch from "node-fetch-cache";
fetch('http://google.com') fetch("http://google.com")
.then(response => response.text()) .then((response) => response.text())
.then(text => console.log(text)); .then((text) => console.log(text));
``` ```
The next time you `fetch('http://google.com')`, the response will be returned from the cache. No HTTP request will be made. The next time you `fetch('http://google.com')`, the response will be returned from the cache. No HTTP request will be made.
@ -33,25 +35,20 @@ This function can be used to eject the response from the cache, so that the next
This module caches ALL responses, even those with 4xx and 5xx response statuses. You can use this function to uncache such responses if desired. For example: This module caches ALL responses, even those with 4xx and 5xx response statuses. You can use this function to uncache such responses if desired. For example:
```js ```js
const fetch = require('node-fetch-cache'); import fetch from "node-fetch-cache";
fetch('http://google.com') fetch("http://google.com")
.then(async response => { .then(async (response) => {
if (!response.ok) { if (!response.ok) {
await response.ejectFromCache(); await response.ejectFromCache();
throw new Error('Non-okay response from google.com'); throw new Error("Non-okay response from google.com");
} else { } else {
return response.text(); return response.text();
} }
}).then(text => console.log(text)); })
.then((text) => console.log(text));
``` ```
## Streaming
This module does not support Stream request bodies, except for fs.ReadStream. And when using fs.ReadStream, the cache key is generated based only on the path of the stream, not its content. That means if you stream `/my/desktop/image.png` twice, you will get a cached response the second time, **even if the content of image.png has changed**.
Streams don't quite play nice with the concept of caching based on request characteristics, because we would have to read the stream to the end to find out what's in it and hash it into a proper cache key.
## Cache Customization ## Cache Customization
By default responses are cached in memory, but you can also cache to files on disk, or implement your own cache. By default responses are cached in memory, but you can also cache to files on disk, or implement your own cache.
@ -63,7 +60,7 @@ This is the default cache delegate. It caches responses in-process in a POJO.
Usage: Usage:
```js ```js
const { fetchBuilder, MemoryCache } = require('node-fetch-cache'); import { fetchBuilder, MemoryCache } from "node-fetch-cache";
const fetch = fetchBuilder.withCache(new MemoryCache(options)); const fetch = fetchBuilder.withCache(new MemoryCache(options));
``` ```
@ -75,7 +72,7 @@ Options:
} }
``` ```
Note that by default (if you don't use `withCache()`) a **shared** MemoryCache will be used (you can import this module in multiple files and they will all share the same cache). If you instantiate and provide a `new MemoryCache()` as shown above however, the cache is *NOT* shared unless you explicitly pass it around and pass it into `withCache()` in each of your source files. Note that by default (if you don't use `withCache()`) a **shared** MemoryCache will be used (you can import this module in multiple files and they will all share the same cache). If you instantiate and provide a `new MemoryCache()` as shown above however, the cache is _NOT_ shared unless you explicitly pass it around and pass it into `withCache()` in each of your source files.
### FileSystemCache ### FileSystemCache
@ -84,7 +81,7 @@ Cache to a directory on disk. This allows the cache to survive the process exiti
Usage: Usage:
```js ```js
const { fetchBuilder, FileSystemCache } = require('node-fetch-cache'); import { fetchBuilder, FileSystemCache } from "node-fetch-cache";
const fetch = fetchBuilder.withCache(new FileSystemCache(options)); const fetch = fetchBuilder.withCache(new FileSystemCache(options));
``` ```
@ -111,6 +108,61 @@ The remove function should accept a key and remove the cached value associated w
All three functions may be async. All three functions may be async.
## Misc Tips
### Streaming
This module does not support Stream request bodies, except for fs.ReadStream. And when using fs.ReadStream, the cache key is generated based only on the path of the stream, not its content. That means if you stream `/my/desktop/image.png` twice, you will get a cached response the second time, **even if the content of image.png has changed**.
Streams don't quite play nice with the concept of caching based on request characteristics, because we would have to read the stream to the end to find out what's in it and hash it into a proper cache key.
### Request Concurrency
Requests with the same cache key are queued. For example, you might wonder if making the same request 100 times simultaneously would result in 100 HTTP requests:
```js
import fetch from "node-fetch-cache";
await Promise.all(
Array(100)
.fill()
.map(() => fetch("https://google.com"))
);
```
The answer is no. Only one request would be made, and 99 of the `fetch()`s will read the response from the cache.
### Cache-Control: only-if-cached Requests
The HTTP standard describes a [Cache-Control request header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#request_directives) to control certain aspects of cache behavior. Node-fetch ignores these, but node-fetch-cache respects the `Cache-Control: only-if-cached` directive. When `only-if-cached` is specified, node-fetch-cache will return `undefined` if there is no cached response. No HTTP request will be made. For example:
```js
import fetch from "node-fetch-cache";
const response = await fetch("https://google.com", {
headers: { "Cache-Control": "only-if-cached" },
});
if (response === undefined) {
// No response was found in the cache
}
```
Note that this is slightly different from browser fetch, which returns a `504 Gateway Timeout` response if no cached response is available.
### Calculating the Cache Key
This module exports a `getCacheKey()` function to calculate a cache key string from request parameters, which may be useful for enabling some advanced use cases (especially if you want to call cache functions directly). Call `getCacheKey()` exactly like you would call `fetch()`.
```js
import { fetchBuilder, MemoryCache, getCacheKey } from "node-fetch-cache";
const cache = new MemoryCache();
const fetch = fetchBuilder.withCache(cache);
const rawCacheData = await cache.get(getCacheKey("https://google.com"));
```
## Bugs / Help / Feature Requests / Contributing ## Bugs / Help / Feature Requests / Contributing
For feature requests or help, please visit [the discussions page on GitHub](https://github.com/mistval/node-fetch-cache/discussions). For feature requests or help, please visit [the discussions page on GitHub](https://github.com/mistval/node-fetch-cache/discussions).

7008
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +1,28 @@
{ {
"name": "node-fetch-cache", "name": "@fascinated/node-fetch-cache",
"version": "3.0.0", "version": "3.1.3",
"description": "node-fetch with a persistent cache.", "description": "node-fetch with caching.",
"main": "src/index.js", "main": "src/index.js",
"type": "module", "type": "module",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"exports": { "exports": {
"import": "./src/index.js", "import": "./src/index.js",
"require": "./commonjs/wrapper.cjs" "require": "./commonjs/wrapper.cjs"
}, },
"scripts": { "scripts": {
"buildcjs": "rollup src/index.js --file commonjs/index.cjs --format cjs", "buildcjs": "rollup src/index.js --file commonjs/index.cjs --format cjs",
"test": "npm run buildcjs && mocha --timeout 10000 --exit", "test": "npm run lintfix && npm run buildcjs && mocha --timeout 10000 --exit",
"coverage": "nyc --reporter=lcov --reporter=text npm test", "coverage": "nyc --reporter=lcov --reporter=text npm test",
"lint": "./node_modules/.bin/eslint .", "lint": "eslint .",
"lintfix": "./node_modules/.bin/eslint . --fix", "lintfix": "eslint . --fix",
"prepublishOnly": "npm test" "prepublishOnly": "npm test"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/mistval/node-fetch-cache.git" "url": "git+https://git.fascinated.cc/Fascinated/node-fetch-cache.git"
}, },
"keywords": [ "keywords": [
"node", "node",
@ -30,24 +34,24 @@
"author": "mistval", "author": "mistval",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/mistval/node-fetch-cache/issues" "url": "https://git.fascinated.cc/Fascinated/node-fetch-cache/issues"
}, },
"homepage": "https://github.com/mistval/node-fetch-cache#readme", "homepage": "https://git.fascinated.cc/Fascinated/node-fetch-cache#readme",
"devDependencies": { "devDependencies": {
"eslint": "^6.8.0", "eslint": "^8.9.0",
"eslint-config-airbnb-base": "^14.1.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.20.2", "eslint-plugin-import": "^2.25.4",
"form-data": "^3.0.0", "form-data": "^3.0.0",
"husky": "^4.3.0", "husky": "^8.0.3",
"mocha": "^8.2.1", "mocha": "^9.2.1",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^2.53.0" "rollup": "^2.53.0"
}, },
"dependencies": { "dependencies": {
"cacache": "^15.2.0", "cacache": "^15.2.0",
"locko": "0.0.3", "locko": "^1.0.0",
"node-fetch": "2.6.1" "node-fetch": "2.6.11"
}, },
"husky": { "husky": {
"hooks": { "hooks": {

6
renovate.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>Fascinated/renovate-config"
]
}

View File

@ -1,6 +1,5 @@
import cacache from 'cacache'; import cacache from 'cacache';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { KeyTimeout } from './key_timeout.js';
function getBodyAndMetaKeys(key) { function getBodyAndMetaKeys(key) {
return [`${key}body`, `${key}meta`]; return [`${key}body`, `${key}meta`];
@ -9,7 +8,6 @@ function getBodyAndMetaKeys(key) {
export class FileSystemCache { export class FileSystemCache {
constructor(options = {}) { constructor(options = {}) {
this.ttl = options.ttl; this.ttl = options.ttl;
this.keyTimeout = new KeyTimeout();
this.cacheDirectory = options.cacheDirectory || '.cache'; this.cacheDirectory = options.cacheDirectory || '.cache';
} }
@ -24,9 +22,15 @@ export class FileSystemCache {
const metaBuffer = await cacache.get.byDigest(this.cacheDirectory, metaInfo.integrity); const metaBuffer = await cacache.get.byDigest(this.cacheDirectory, metaInfo.integrity);
const metaData = JSON.parse(metaBuffer); const metaData = JSON.parse(metaBuffer);
const { bodyStreamIntegrity, empty } = metaData; const { bodyStreamIntegrity, empty, expiration } = metaData;
delete metaData.bodyStreamIntegrity; delete metaData.bodyStreamIntegrity;
delete metaData.empty; delete metaData.empty;
delete metaData.expiration;
if (expiration && expiration < Date.now()) {
return undefined;
}
const bodyStream = empty const bodyStream = empty
? Readable.from(Buffer.alloc(0)) ? Readable.from(Buffer.alloc(0))
@ -41,8 +45,6 @@ export class FileSystemCache {
remove(key) { remove(key) {
const [bodyKey, metaKey] = getBodyAndMetaKeys(key); const [bodyKey, metaKey] = getBodyAndMetaKeys(key);
this.keyTimeout.clearTimeout(key);
return Promise.all([ return Promise.all([
cacache.rm.entry(this.cacheDirectory, bodyKey), cacache.rm.entry(this.cacheDirectory, bodyKey),
cacache.rm.entry(this.cacheDirectory, metaKey), cacache.rm.entry(this.cacheDirectory, metaKey),
@ -53,7 +55,9 @@ export class FileSystemCache {
const [bodyKey, metaKey] = getBodyAndMetaKeys(key); const [bodyKey, metaKey] = getBodyAndMetaKeys(key);
const metaCopy = { ...metaData }; const metaCopy = { ...metaData };
this.keyTimeout.clearTimeout(key); if (typeof this.ttl === 'number') {
metaCopy.expiration = Date.now() + this.ttl;
}
try { try {
metaCopy.bodyStreamIntegrity = await new Promise((fulfill, reject) => { metaCopy.bodyStreamIntegrity = await new Promise((fulfill, reject) => {
@ -75,10 +79,6 @@ export class FileSystemCache {
await cacache.put(this.cacheDirectory, metaKey, metaBuffer); await cacache.put(this.cacheDirectory, metaKey, metaBuffer);
const cachedData = await this.get(key); const cachedData = await this.get(key);
if (typeof this.ttl === 'number') {
this.keyTimeout.updateTimeout(key, this.ttl, () => this.remove(key));
}
return cachedData; return cachedData;
} }
} }

View File

@ -1,12 +1,11 @@
import fetch, { Request } from 'node-fetch'; import fetch, { Request } from 'node-fetch';
import fs from 'fs'; import fs from 'fs';
import { URLSearchParams } from 'url';
import crypto from 'crypto'; import crypto from 'crypto';
import locko from 'locko'; import locko from 'locko';
import { NFCResponse } from './classes/response.js'; import { NFCResponse } from './classes/response.js';
import { MemoryCache } from './classes/caching/memory_cache.js'; import { MemoryCache } from './classes/caching/memory_cache.js';
const CACHE_VERSION = 3; const CACHE_VERSION = 4;
function md5(str) { function md5(str) {
return crypto.createHash('md5').update(str).digest('hex'); return crypto.createHash('md5').update(str).digest('hex');
@ -36,6 +35,14 @@ function getFormDataCacheKey(formData) {
return cacheKey; return cacheKey;
} }
function getHeadersCacheKeyJson(headersObj) {
return Object.fromEntries(
Object.entries(headersObj)
.map(([key, value]) => [key.toLowerCase(), value])
.filter(([key, value]) => key !== 'cache-control' || value !== 'only-if-cached'),
);
}
function getBodyCacheKeyJson(body) { function getBodyCacheKeyJson(body) {
if (!body) { if (!body) {
return body; return body;
@ -55,11 +62,13 @@ function getBodyCacheKeyJson(body) {
} }
function getRequestCacheKey(req) { function getRequestCacheKey(req) {
const headersPojo = Object.fromEntries([...req.headers.entries()]);
return { return {
cache: req.cache, cache: req.cache,
credentials: req.credentials, credentials: req.credentials,
destination: req.destination, destination: req.destination,
headers: req.headers, headers: getHeadersCacheKeyJson(headersPojo),
integrity: req.integrity, integrity: req.integrity,
method: req.method, method: req.method,
redirect: req.redirect, redirect: req.redirect,
@ -70,15 +79,15 @@ function getRequestCacheKey(req) {
}; };
} }
function getCacheKey(requestArguments) { export function getCacheKey(resource, init = {}) {
const resource = requestArguments[0];
const init = requestArguments[1] || {};
const resourceCacheKeyJson = resource instanceof Request const resourceCacheKeyJson = resource instanceof Request
? getRequestCacheKey(resource) ? getRequestCacheKey(resource)
: { url: resource }; : { url: resource };
const initCacheKeyJson = { ...init }; const initCacheKeyJson = {
...init,
headers: getHeadersCacheKeyJson(init.headers || {}),
};
resourceCacheKeyJson.body = getBodyCacheKeyJson(resourceCacheKeyJson.body); resourceCacheKeyJson.body = getBodyCacheKeyJson(resourceCacheKeyJson.body);
initCacheKeyJson.body = getBodyCacheKeyJson(initCacheKeyJson.body); initCacheKeyJson.body = getBodyCacheKeyJson(initCacheKeyJson.body);
@ -88,8 +97,25 @@ function getCacheKey(requestArguments) {
return md5(JSON.stringify([resourceCacheKeyJson, initCacheKeyJson, CACHE_VERSION])); return md5(JSON.stringify([resourceCacheKeyJson, initCacheKeyJson, CACHE_VERSION]));
} }
function hasOnlyWithCacheOption(resource, init) {
if (
init
&& init.headers
&& Object.entries(init.headers)
.some(([key, value]) => key.toLowerCase() === 'cache-control' && value === 'only-if-cached')
) {
return true;
}
if (resource instanceof Request && resource.headers.get('Cache-Control') === 'only-if-cached') {
return true;
}
return false;
}
async function getResponse(cache, requestArguments) { async function getResponse(cache, requestArguments) {
const cacheKey = getCacheKey(requestArguments); const cacheKey = getCacheKey(...requestArguments);
let cachedValue = await cache.get(cacheKey); let cachedValue = await cache.get(cacheKey);
const ejectSelfFromCache = () => cache.remove(cacheKey); const ejectSelfFromCache = () => cache.remove(cacheKey);
@ -103,6 +129,10 @@ async function getResponse(cache, requestArguments) {
); );
} }
if (hasOnlyWithCacheOption(...requestArguments)) {
return undefined;
}
await locko.lock(cacheKey); await locko.lock(cacheKey);
try { try {
cachedValue = await cache.get(cacheKey); cachedValue = await cache.get(cacheKey);

View File

@ -7,7 +7,7 @@ import rimraf from 'rimraf';
import path from 'path'; import path from 'path';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
import standardFetch from 'node-fetch'; import standardFetch from 'node-fetch';
import FetchCache, { MemoryCache, FileSystemCache } from '../src/index.js'; import FetchCache, { MemoryCache, FileSystemCache, getCacheKey } from '../src/index.js';
import { Agent } from 'http'; import { Agent } from 'http';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@ -326,6 +326,17 @@ describe('Data tests', function() {
assert.strictEqual(res.fromCache, true); assert.strictEqual(res.fromCache, true);
}); });
it('Supports request objects with custom headers', async function() {
const request1 = new standardFetch.Request(TWO_HUNDRED_URL, { headers: { 'XXX': 'YYY' } });
const request2 = new standardFetch.Request(TWO_HUNDRED_URL, { headers: { 'XXX': 'ZZZ' } });
res = await cachedFetch(request1);
assert.strictEqual(res.fromCache, false);
res = await cachedFetch(request2);
assert.strictEqual(res.fromCache, false);
});
it('Refuses to consume body twice', async function() { it('Refuses to consume body twice', async function() {
res = await cachedFetch(TEXT_BODY_URL); res = await cachedFetch(TEXT_BODY_URL);
await res.text(); await res.text();
@ -476,3 +487,45 @@ describe('File system cache tests', function() {
assert.strictEqual(res.fromCache, true); assert.strictEqual(res.fromCache, true);
}); });
}); });
describe('Cache mode tests', function() {
it('Can use the only-if-cached cache control setting via init', async function() {
res = await cachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } });
assert(!res);
res = await cachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } });
assert(!res);
res = await cachedFetch(TWO_HUNDRED_URL);
assert(res && !res.fromCache);
res = await cachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } });
assert(res && res.fromCache);
await res.ejectFromCache();
res = await cachedFetch(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } });
assert(!res);
});
it('Can use the only-if-cached cache control setting via resource', async function() {
res = await cachedFetch(new standardFetch.Request(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }));
assert(!res);
res = await cachedFetch(new standardFetch.Request(TWO_HUNDRED_URL));
assert(res && !res.fromCache);
res = await cachedFetch(new standardFetch.Request(TWO_HUNDRED_URL, { headers: { 'Cache-Control': 'only-if-cached' } }));
assert(res && res.fromCache);
});
});
describe('Cache key tests', function() {
it('Can calculate a cache key and check that it exists', async function() {
const cache = new MemoryCache();
cachedFetch = FetchCache.withCache(cache);
await cachedFetch(TWO_HUNDRED_URL);
const cacheKey = getCacheKey(TWO_HUNDRED_URL);
const nonExistentCacheKey = getCacheKey(TEXT_BODY_URL);
const cacheKeyResult = await cache.get(cacheKey);
const nonExistentCacheKeyResult = await cache.get(nonExistentCacheKey);
assert(cacheKeyResult);
assert(!nonExistentCacheKeyResult);
});
});