Skip to content

Commit a4c3a1a

Browse files
committed
Fix bug with not properly de-duping fetches with custom fetching function sources #32 Fixes #52
1 parent b51e55e commit a4c3a1a

File tree

4 files changed

+103
-43
lines changed

4 files changed

+103
-43
lines changed

eleventy-fetch.js

+13-6
Original file line numberDiff line numberDiff line change
@@ -67,21 +67,28 @@ queue.on("active", () => {
6767

6868
let inProgress = {};
6969

70-
function queueSave(source, queueCallback) {
71-
if (!inProgress[source]) {
72-
inProgress[source] = queue.add(queueCallback).finally(() => {
73-
delete inProgress[source];
70+
function queueSave(source, queueCallback, options) {
71+
let sourceKey;
72+
if(typeof source === "string") {
73+
sourceKey = source;
74+
} else {
75+
sourceKey = RemoteAssetCache.getUid(source, options);
76+
}
77+
78+
if (!inProgress[sourceKey]) {
79+
inProgress[sourceKey] = queue.add(queueCallback).finally(() => {
80+
delete inProgress[sourceKey];
7481
});
7582
}
7683

77-
return inProgress[source];
84+
return inProgress[sourceKey];
7885
}
7986

8087
module.exports = function (source, options) {
8188
let mergedOptions = Object.assign({}, globalOptions, options);
8289
return queueSave(source, () => {
8390
return save(source, mergedOptions);
84-
});
91+
}, mergedOptions);
8592
};
8693

8794
Object.defineProperty(module.exports, "concurrency", {

src/AssetCache.js

+18-8
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,32 @@ const debug = require("debug")("Eleventy:Fetch");
99
class AssetCache {
1010
#customFilename;
1111

12-
constructor(url, cacheDirectory, options = {}) {
13-
let uniqueKey;
12+
static getCacheKey(source, options) {
13+
// RemoteAssetCache sends this an Array, which skips this altogether
1414
if (
15-
(typeof url === "object" && typeof url.then === "function") ||
16-
(typeof url === "function" && url.constructor.name === "AsyncFunction")
15+
(typeof source === "object" && typeof source.then === "function") ||
16+
(typeof source === "function" && source.constructor.name === "AsyncFunction")
1717
) {
1818
if(typeof options.formatUrlForDisplay !== "function") {
19-
throw new Error("When caching an arbitrary promise source, options.formatUrlForDisplay is required.");
19+
throw new Error("When caching an arbitrary promise source, an options.formatUrlForDisplay() callback is required.");
2020
}
2121

22-
uniqueKey = options.formatUrlForDisplay();
23-
} else {
24-
uniqueKey = url;
22+
return options.formatUrlForDisplay();
2523
}
2624

25+
return source;
26+
}
27+
28+
constructor(url, cacheDirectory, options = {}) {
29+
let uniqueKey;
30+
// RemoteAssetCache passes in an array
31+
if(Array.isArray(uniqueKey)) {
32+
uniqueKey = uniqueKey.join(",");
33+
} else {
34+
uniqueKey = AssetCache.getCacheKey(url, options);
35+
}
2736
this.uniqueKey = uniqueKey;
37+
2838
this.hash = AssetCache.getHash(uniqueKey, options.hashLength);
2939
this.cacheDirectory = cacheDirectory || ".cache";
3040
this.defaultDuration = "1d";

src/RemoteAssetCache.js

+41-27
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,34 @@
1-
const fs = require("fs");
2-
const fsp = fs.promises; // Node 10+
3-
41
const AssetCache = require("./AssetCache");
5-
const debug = require("debug")("Eleventy:Fetch");
2+
// const debug = require("debug")("Eleventy:Fetch");
63

74
class RemoteAssetCache extends AssetCache {
85
constructor(url, cacheDirectory, options = {}) {
96
let cleanUrl = url;
107
if (options.removeUrlQueryParams) {
11-
if (typeof cleanUrl !== "string") {
12-
throw new Error(
13-
"The `removeUrlQueryParams` option requires the cache source to be a string. Received: " +
14-
typeof url,
15-
);
16-
}
17-
188
cleanUrl = RemoteAssetCache.cleanUrl(cleanUrl);
199
}
2010

21-
let cacheKey = [cleanUrl];
11+
// Must run after removeUrlQueryParams
12+
let displayUrl = RemoteAssetCache.convertUrlToString(cleanUrl, options);
13+
let cacheKeyArray = RemoteAssetCache.getCacheKey(displayUrl, options);
14+
15+
super(cacheKeyArray, cacheDirectory, options);
16+
17+
this.url = url;
18+
this.options = options;
19+
20+
this.displayUrl = displayUrl;
21+
}
22+
23+
static getUid(source, options) {
24+
let displayUrl = RemoteAssetCache.convertUrlToString(source, options);
25+
let cacheKeyArray = RemoteAssetCache.getCacheKey(displayUrl, options);
26+
return cacheKeyArray.join(",");
27+
}
28+
29+
static getCacheKey(source, options) {
30+
// Promise sources are handled upstream
31+
let cacheKey = [source];
2232

2333
if (options.fetchOptions) {
2434
if (options.fetchOptions.method && options.fetchOptions.method !== "GET") {
@@ -29,29 +39,33 @@ class RemoteAssetCache extends AssetCache {
2939
}
3040
}
3141

32-
super(cacheKey, cacheDirectory, options);
33-
34-
this.url = url;
35-
this.options = options;
36-
37-
// Important: runs after removeUrlQueryParams
38-
this.displayUrl = this.formatUrlForDisplay(cleanUrl);
42+
return cacheKey;
3943
}
4044

4145
static cleanUrl(url) {
42-
let cleanUrl = new URL(url);
46+
if(typeof url !== "string" && !(url instanceof URL)) {
47+
return url;
48+
}
49+
50+
let cleanUrl;
51+
if(typeof url === "string") {
52+
cleanUrl = new URL(url);
53+
} else if(url instanceof URL) {
54+
cleanUrl = url;
55+
}
56+
4357
cleanUrl.search = new URLSearchParams([]);
58+
4459
return cleanUrl.toString();
4560
}
4661

47-
formatUrlForDisplay(url) {
48-
if (
49-
this.options.formatUrlForDisplay &&
50-
typeof this.options.formatUrlForDisplay === "function"
51-
) {
52-
return this.options.formatUrlForDisplay(url);
62+
static convertUrlToString(source, options = {}) {
63+
let { formatUrlForDisplay } = options;
64+
if (formatUrlForDisplay && typeof formatUrlForDisplay === "function") {
65+
return formatUrlForDisplay(source);
5366
}
54-
return url;
67+
68+
return source;
5569
}
5670

5771
get url() {

test/QueueTest.js

+31-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ const fs = require("fs");
33
const Cache = require("../");
44
const RemoteAssetCache = require("../src/RemoteAssetCache");
55

6-
let fsp = fs.promises;
7-
86
test("Double Fetch", async (t) => {
97
let pngUrl = "https://www.zachleat.com/img/avatar-2017-big.png";
108
let ac1 = Cache(pngUrl);
@@ -40,3 +38,34 @@ test("Double Fetch (dry run)", async (t) => {
4038
// file is now accessible
4139
t.false(forTestOnly.hasCacheFiles());
4240
});
41+
42+
test("Double Fetch async function (dry run)", async (t) => {
43+
let expected = { mockKey: "mockValue" };
44+
45+
async function fetch() {
46+
return Promise.resolve(expected);
47+
};
48+
49+
let ac1 = Cache(fetch, {
50+
dryRun: true,
51+
formatUrlForDisplay() {
52+
return "fetch-1";
53+
},
54+
});
55+
let ac2 = Cache(fetch, {
56+
dryRun: true,
57+
formatUrlForDisplay() {
58+
return "fetch-2";
59+
},
60+
});
61+
62+
// Make sure we only fetch once!
63+
t.not(ac1, ac2);
64+
65+
let result1 = await ac1;
66+
let result2 = await ac2;
67+
68+
t.deepEqual(result1, result2);
69+
t.deepEqual(result1, expected);
70+
t.deepEqual(result2, expected);
71+
});

0 commit comments

Comments
 (0)