Skip to content

Commit

Permalink
Keep cached contents in memory for AssetCache instance. Renames `wasL…
Browse files Browse the repository at this point in the history
…astFetchHit` to `wasLastFetchCacheHit`. Cache fetch promise.
  • Loading branch information
zachleat committed Dec 19, 2024
1 parent f7c6695 commit 4623ebb
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 18 deletions.
48 changes: 32 additions & 16 deletions src/AssetCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ const { createHash } = require("crypto");

const Sources = require("./Sources.js");

const debug = require("debug")("Eleventy:Fetch");
const debugUtil = require("debug");
const debug = debugUtil("Eleventy:Fetch");
const debugAssets = debugUtil("Eleventy:Assets");

class AssetCache {
#source;
Expand All @@ -15,6 +17,7 @@ class AssetCache {
#cacheDirectory;
#cacheLocationDirty = false;
#dirEnsured = false;
#rawContents = {}

constructor(source, cacheDirectory, options = {}) {
if(!Sources.isValidSource(source)) {
Expand Down Expand Up @@ -232,32 +235,36 @@ class AssetCache {
throw new Error("save(contents) expects contents (was falsy)");
}

this.cache.set(this.hash, {
cachedAt: Date.now(),
type: type,
metadata,
});

let contentPath = this.getCachedContentsPath(type);

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

this.#rawContents[type] = contents;

if(this.options.dryRun) {
debug(`Dry run writing ${contentPath}`);
return;
}

this.ensureDir();

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

debugAssets("Writing cache file to disk for %o", this.source)
// the contents must exist before the cache metadata are saved below
fs.writeFileSync(contentPath, contents);
debug(`Writing ${contentPath}`);

this.cache.set(this.hash, {
cachedAt: Date.now(),
type: type,
metadata,
});

this.cache.save();
}

async getCachedContents(type) {
async #getCachedContents(type) {
let contentPath = this.getCachedContentsPath(type);
debug(`Fetching from cache ${contentPath}`);

Expand All @@ -268,6 +275,15 @@ class AssetCache {
return fs.readFileSync(contentPath, type !== "buffer" ? "utf8" : null);
}

getCachedContents(type) {
if(!this.#rawContents[type]) {
this.#rawContents[type] = this.#getCachedContents(type);
}

// already saved on this instance in-memory
return this.#rawContents[type];
}

_backwardsCompatibilityGetCachedValue(type) {
if (type === "json") {
return this.cachedObject.contents;
Expand Down Expand Up @@ -341,16 +357,16 @@ class AssetCache {
return !this.isCacheValid(duration);
}

async fetch(options) {
if (this.isCacheValid(options.duration)) {
// This is only included for completenes—not on the docs.
async fetch(optionsOverride = {}) {
if (this.isCacheValid(optionsOverride.duration)) {
// promise
this.log(`Using cached version of: ${this.uniqueKey}`);
return this.getCachedValue();
}

this.log(`Saving ${this.uniqueKey} to ${this.cacheFilename}`);

await this.save(this.source, options.type);
await this.save(this.source, optionsOverride.type);

return this.source;
}
Expand Down
18 changes: 16 additions & 2 deletions src/RemoteAssetCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const assetDebug = require("debug")("Eleventy:Assets");
class RemoteAssetCache extends AssetCache {
#queue;
#queuePromise;
#fetchPromise;
#lastFetchType;

constructor(source, cacheDirectory, options = {}) {
Expand Down Expand Up @@ -126,11 +127,11 @@ class RemoteAssetCache extends AssetCache {

// if last fetch was a cache hit (no fetch occurred) or a cache miss (fetch did occur)
// used by Eleventy Image in disk cache checks.
wasLastFetchHit() {
wasLastFetchCacheHit() {
return this.#lastFetchType === "hit";
}

async fetch(optionsOverride = {}) {
async #fetch(optionsOverride = {}) {
// Important: no disk writes when dryRun
// As of Fetch v4, reads are now allowed!
if (this.isCacheValid(optionsOverride.duration)) {
Expand Down Expand Up @@ -203,5 +204,18 @@ class RemoteAssetCache extends AssetCache {
}
}
}

// async but not explicitly declared for promise equality checks
// returns a Promise
fetch(optionsOverride = {}) {
if(!this.#fetchPromise) {
// one at a time. clear when finished
this.#fetchPromise = this.#fetch(optionsOverride).finally(() => {
this.#fetchPromise = undefined;
});
}

return this.#fetchPromise;
}
}
module.exports = RemoteAssetCache;
101 changes: 101 additions & 0 deletions test/QueueTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,104 @@ test("Docs example https://www.11ty.dev/docs/plugins/fetch/#manually-store-your-
followerCount: 1000
});
});

test("Raw Fetch using queue method", async (t) => {
let pngUrl = "https://www.zachleat.com/img/avatar-2017.png?q=1";
let ac1 = Fetch(pngUrl);
let ac2 = Fetch(pngUrl);

// Destroy to clear any existing cache
try {
await ac1.destroy();
} catch (e) {}
try {
await ac2.destroy();
} catch (e) {}

// Make sure the instance is the same
t.is(ac1, ac2);

let result1 = await ac1.queue();
t.false(ac1.wasLastFetchCacheHit())

let result2 = await ac1.queue();
// reuses the same fetch
t.false(ac1.wasLastFetchCacheHit())

t.is(result1, result2);

// file is now accessible
try {
await ac1.destroy();
} catch (e) {}
try {
await ac2.destroy();
} catch (e) {}
});


test("Raw Fetch using fetch method", async (t) => {
let pngUrl = "https://www.zachleat.com/img/avatar-2017.png?q=2";
let ac1 = Fetch(pngUrl);
let ac2 = Fetch(pngUrl);

// Destroy to clear any existing cache
try {
await ac1.destroy();
} catch (e) {}
try {
await ac2.destroy();
} catch (e) {}

// Make sure the instance is the same
t.is(ac1, ac2);

let result1 = await ac1.fetch();
t.false(ac1.wasLastFetchCacheHit())

let result2 = await ac1.fetch();
t.true(ac1.wasLastFetchCacheHit())

t.is(result1, result2);

// file is now accessible
try {
await ac1.destroy();
} catch (e) {}
try {
await ac2.destroy();
} catch (e) {}
});

test("Raw Fetch using fetch method (check parallel fetch promise reuse)", async (t) => {
let pngUrl = "https://www.zachleat.com/img/avatar-2017.png?q=3";
let ac1 = Fetch(pngUrl);
let ac2 = Fetch(pngUrl);

// Destroy to clear any existing cache
try {
await ac1.destroy();
} catch (e) {}
try {
await ac2.destroy();
} catch (e) {}

// Make sure the instance is the same
t.is(ac1, ac2);

let fetch1 = ac1.fetch();
let fetch2 = ac1.fetch();
t.is(fetch1, fetch2);

t.is(await fetch1, await fetch2);

t.false(ac1.wasLastFetchCacheHit())

// file is now accessible
try {
await ac1.destroy();
} catch (e) {}
try {
await ac2.destroy();
} catch (e) {}
});

0 comments on commit 4623ebb

Please sign in to comment.