Skip to content

Commit c608520

Browse files
committed
Adds wasLastFetchHit method used by Eleventy Image plugin to invalidate disk cache. Removes setInitialCacheTimestamp which is no longer needed for this use case (and wasn’t released). Changes in memory cache to persist instances after requests complete.
1 parent 51311c8 commit c608520

File tree

5 files changed

+87
-86
lines changed

5 files changed

+87
-86
lines changed

eleventy-fetch.js

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,33 +34,34 @@ queue.on("active", () => {
3434
debug(`Concurrency: ${queue.concurrency}, Size: ${queue.size}, Pending: ${queue.pending}`);
3535
});
3636

37-
let inProgress = {};
37+
let instCache = {};
3838

39-
function queueSave(source, queueCallback, options) {
39+
function createRemoteAssetCache(source, rawOptions = {}) {
40+
if (!Sources.isFullUrl(source) && !Sources.isValidSource(source)) {
41+
return Promise.reject(new Error("Invalid source. Received: " + source));
42+
}
43+
44+
let options = Object.assign({}, globalOptions, rawOptions);
4045
let sourceKey = RemoteAssetCache.getRequestId(source, options);
4146
if(!sourceKey) {
4247
return Promise.reject(Sources.getInvalidSourceError(source));
4348
}
4449

45-
if (!inProgress[sourceKey]) {
46-
inProgress[sourceKey] = queue.add(queueCallback).finally(() => {
47-
delete inProgress[sourceKey];
48-
});
50+
if(instCache[sourceKey]) {
51+
return instCache[sourceKey];
4952
}
5053

51-
return inProgress[sourceKey];
54+
let inst = new RemoteAssetCache(source, options.directory, options);
55+
inst.setQueue(queue);
56+
57+
instCache[sourceKey] = inst;
58+
59+
return inst;
5260
}
5361

5462
module.exports = function (source, options) {
55-
if (!Sources.isFullUrl(source) && !Sources.isValidSource(source)) {
56-
throw new Error("Caching an already local asset is not yet supported.");
57-
}
58-
59-
let mergedOptions = Object.assign({}, globalOptions, options);
60-
return queueSave(source, () => {
61-
let asset = new RemoteAssetCache(source, mergedOptions.directory, mergedOptions);
62-
return asset.fetch(mergedOptions);
63-
}, mergedOptions);
63+
let instance = createRemoteAssetCache(source, options);
64+
return instance.queue();
6465
};
6566

6667
Object.defineProperty(module.exports, "concurrency", {
@@ -72,7 +73,15 @@ Object.defineProperty(module.exports, "concurrency", {
7273
},
7374
});
7475

75-
module.exports.queue = queueSave;
76+
module.exports.Fetch = createRemoteAssetCache;
77+
78+
// Deprecated API kept for backwards compat, instead: use default export directly.
79+
// Intentional: queueCallback is ignored here
80+
module.exports.queue = function(source, queueCallback, options) {
81+
let instance = createRemoteAssetCache(source, options);
82+
return instance.queue();
83+
};
84+
7685
module.exports.Util = {
7786
isFullUrl: Sources.isFullUrl,
7887
};

src/AssetCache.js

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ class AssetCache {
2626
this.hash = AssetCache.getHash(uniqueKey, options.hashLength);
2727

2828
this.cacheDirectory = cacheDirectory || ".cache";
29-
this.defaultDuration = "1d";
3029
this.options = options;
3130

31+
this.defaultDuration = "1d";
32+
this.duration = options.duration || this.defaultDuration;
33+
3234
// Compute the filename only once
3335
if (typeof this.options.filenameFormat === "function") {
3436
this.#customFilename = AssetCache.cleanFilename(this.options.filenameFormat(uniqueKey, this.hash));
@@ -39,10 +41,6 @@ class AssetCache {
3941
}
4042
}
4143

42-
setInitialCacheTimestamp(timestamp) {
43-
this.initialCacheTimestamp = timestamp;
44-
}
45-
4644
log(message) {
4745
if (this.options.verbose) {
4846
console.log(`[11ty/eleventy-fetch] ${message}`);
@@ -234,31 +232,29 @@ class AssetCache {
234232
throw new Error("save(contents) expects contents (was falsy)");
235233
}
236234

235+
let contentPath = this.getCachedContentsPath(type);
236+
if(this.options.dryRun) {
237+
debug(`Dry run writing ${contentPath}`);
238+
return;
239+
}
240+
237241
this.ensureDir();
238242

239243
if (type === "json" || type === "parsed-xml") {
240244
contents = JSON.stringify(contents);
241245
}
242246

243-
let contentPath = this.getCachedContentsPath(type);
244-
245247
// the contents must exist before the cache metadata are saved below
246-
if(!this.options.dryRun) {
247-
fs.writeFileSync(contentPath, contents);
248-
debug(`Writing ${contentPath}`);
249-
} else {
250-
debug(`Dry run writing ${contentPath}`);
251-
}
248+
fs.writeFileSync(contentPath, contents);
249+
debug(`Writing ${contentPath}`);
252250

253251
this.cache.set(this.hash, {
254-
cachedAt: this.initialCacheTimestamp || Date.now(),
252+
cachedAt: Date.now(),
255253
type: type,
256254
metadata,
257255
});
258256

259-
if(!this.options.dryRun) {
260-
this.cache.save();
261-
}
257+
this.cache.save();
262258
}
263259

264260
async getCachedContents(type) {
@@ -304,10 +300,10 @@ class AssetCache {
304300
}
305301

306302
getCachedTimestamp() {
307-
return this.cachedObject?.cachedAt || this.initialCacheTimestamp;
303+
return this.cachedObject?.cachedAt;
308304
}
309305

310-
isCacheValid(duration = this.defaultDuration) {
306+
isCacheValid(duration = this.duration) {
311307
if (!this.cachedObject) {
312308
// not cached
313309
return false;

src/RemoteAssetCache.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ const { parseXml } = require('@rgrove/parse-xml');
22

33
const Sources = require("./Sources.js");
44
const AssetCache = require("./AssetCache.js");
5-
// const debug = require("debug")("Eleventy:Fetch");
5+
const assetDebug = require("debug")("Eleventy:Assets");
66

77
class RemoteAssetCache extends AssetCache {
8+
#queue;
9+
#queuePromise;
10+
#lastFetchType;
11+
812
constructor(source, cacheDirectory, options = {}) {
913
let requestId = RemoteAssetCache.getRequestId(source, options);
1014
super(requestId, cacheDirectory, options);
@@ -95,15 +99,48 @@ class RemoteAssetCache extends AssetCache {
9599
return Buffer.from(await response.arrayBuffer());
96100
}
97101

102+
setQueue(queue) {
103+
this.#queue = queue;
104+
}
105+
106+
// Returns raw Promise
107+
queue() {
108+
if(!this.#queue) {
109+
throw new Error("Missing `#queue` instance.");
110+
}
111+
112+
if(this.#queuePromise) {
113+
return this.#queuePromise;
114+
}
115+
116+
// optionsOverride not supported on fetch here for re-use
117+
this.#queuePromise = this.#queue.add(() => this.fetch());
118+
119+
return this.#queuePromise;
120+
}
121+
122+
isCacheValid(duration = undefined) {
123+
// uses this.options.duration if not explicitly defined here
124+
return super.isCacheValid(duration);
125+
}
126+
127+
// if last fetch was a cache hit (no fetch occurred) or a cache miss (fetch did occur)
128+
// used by Eleventy Image in disk cache checks.
129+
wasLastFetchHit() {
130+
return this.#lastFetchType === "hit";
131+
}
132+
98133
async fetch(optionsOverride = {}) {
99-
let duration = optionsOverride.duration || this.options.duration;
100134
// Important: no disk writes when dryRun
101135
// As of Fetch v4, reads are now allowed!
102-
if (super.isCacheValid(duration)) {
136+
if (this.isCacheValid(optionsOverride.duration)) {
103137
this.log(`Cache hit for ${this.displayUrl}`);
138+
this.#lastFetchType = "hit";
104139
return super.getCachedValue();
105140
}
106141

142+
this.#lastFetchType = "miss";
143+
107144
try {
108145
let isDryRun = optionsOverride.dryRun || this.options.dryRun;
109146
this.log(`${isDryRun ? "Fetching" : "Cache miss for"} ${this.displayUrl}`);
@@ -124,6 +161,7 @@ class RemoteAssetCache extends AssetCache {
124161

125162
this.fetchCount++;
126163

164+
assetDebug("Fetching remote asset: %o", this.source);
127165
// v5: now using global (Node-native or otherwise) fetch instead of node-fetch
128166
let response = await fetch(this.source, fetchOptions);
129167
if (!response.ok) {

test/AssetCacheTest.js

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -137,45 +137,3 @@ test("Uses filenameFormat", async (t) => {
137137
t.falsy(fs.existsSync(cachePath));
138138
t.falsy(fs.existsSync(jsonCachePath));
139139
});
140-
141-
test("setInitialCacheTimestamp method (used by Eleventy Image to establish a consistent cached file name in synchronous contexts)", async (t) => {
142-
let cache = new AssetCache("this_is_a_test", ".cache", {
143-
dryRun: true
144-
});
145-
let timestamp = (new Date(2024,1,1)).getTime();
146-
cache.setInitialCacheTimestamp(timestamp);
147-
148-
await cache.save("test");
149-
150-
t.is(cache.getCachedTimestamp(), timestamp);
151-
});
152-
153-
test("setInitialCacheTimestamp method after save is ignored", async (t) => {
154-
let cache = new AssetCache("this_is_a_test2", ".cache", {
155-
dryRun: true
156-
});
157-
158-
let timestamp = (new Date(2024,1,1)).getTime();
159-
160-
await cache.save("test");
161-
162-
cache.setInitialCacheTimestamp(timestamp);
163-
164-
t.not(cache.getCachedTimestamp(), timestamp);
165-
});
166-
167-
test("setInitialCacheTimestamp method before a second save is used", async (t) => {
168-
let cache = new AssetCache("this_is_a_test3", ".cache", {
169-
dryRun: true
170-
});
171-
172-
let timestamp = (new Date(2024,1,1)).getTime();
173-
174-
await cache.save("test");
175-
176-
cache.setInitialCacheTimestamp(timestamp);
177-
178-
await cache.save("test");
179-
180-
t.is(cache.getCachedTimestamp(), timestamp);
181-
});

test/QueueTest.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const test = require("ava");
2-
const Cache = require("../");
3-
const queue = Cache.queue;
4-
const RemoteAssetCache = require("../src/RemoteAssetCache");
2+
const Cache = require("../eleventy-fetch.js");
3+
const { queue, Fetch } = Cache;
54

65
test("Queue without options", async (t) => {
76
let example = "https://example.com/";
@@ -28,7 +27,7 @@ test("Double Fetch", async (t) => {
2827
await ac1;
2928
await ac2;
3029

31-
let forDestroyOnly = new RemoteAssetCache(pngUrl);
30+
let forDestroyOnly = Fetch(pngUrl);
3231
// file is now accessible
3332
try {
3433
await forDestroyOnly.destroy();
@@ -46,7 +45,8 @@ test("Double Fetch (dry run)", async (t) => {
4645
await ac1;
4746
await ac2;
4847

49-
let forTestOnly = new RemoteAssetCache(pngUrl, ".cache", {
48+
let forTestOnly = Fetch(pngUrl, {
49+
cacheDirectory: ".cache",
5050
dryRun: true,
5151
});
5252
// file is now accessible

0 commit comments

Comments
 (0)