Skip to content

Commit

Permalink
feat(#23): Pass in a custom fetching function rather than a URL
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-wade committed Mar 9, 2024
1 parent 712c242 commit 80673c8
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 16 deletions.
10 changes: 9 additions & 1 deletion eleventy-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,19 @@ function isFullUrl(url) {
}
}

function isAwaitable(maybeAwaitable) {
return (typeof maybeAwaitable === "object" && typeof maybeAwaitable.then === "function") || (maybeAwaitable.constructor.name === "AsyncFunction");
}

async function save(source, options) {
if(!isFullUrl(source)) {
if(!(isFullUrl(source) || isAwaitable(source))) {
return Promise.reject(new Error("Caching an already local asset is not yet supported."));
}

if (isAwaitable(source) && !options.formatUrlForDisplay) {
return Promise.reject(new Error("formatUrlForDisplay must be implemented, as a Promise has been provided."));
}

let asset = new RemoteAssetCache(source, options.directory, options);
return asset.fetch(options);
}
Expand Down
11 changes: 9 additions & 2 deletions src/AssetCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ const { createHash } = require("crypto");
const debug = require("debug")("EleventyCacheAssets");

class AssetCache {
constructor(uniqueKey, cacheDirectory, options = {}) {
this.uniqueKey = uniqueKey;
constructor(url, cacheDirectory, options = {}) {
let uniqueKey;
if ((typeof url === "object" && typeof url.then === "function") || (typeof url === "function" && url.constructor.name === "AsyncFunction")) {
uniqueKey = options.formatUrlForDisplay();
} else {
uniqueKey = url;
}

this.hash = AssetCache.getHash(uniqueKey, options.hashLength);
this.cacheDirectory = cacheDirectory || ".cache";
this.defaultDuration = "1d";
Expand All @@ -24,6 +30,7 @@ class AssetCache {
}

// Defult hashLength also set in global options, duplicated here for tests
// Default hashLength also set in global options, duplicated here for tests
static getHash(url, hashLength = 30) {
let hash = createHash("sha256");
hash.update(url);
Expand Down
23 changes: 17 additions & 6 deletions src/RemoteAssetCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,24 @@ class RemoteAssetCache extends AssetCache {
this.log( `${isDryRun? "Fetching" : "Cache miss for"} ${this.displayUrl}`);

let fetchOptions = optionsOverride.fetchOptions || this.options.fetchOptions || {};
let response = await fetch(this.url, fetchOptions);
if(!response.ok) {
throw new Error(`Bad response for ${this.displayUrl} (${response.status}): ${response.statusText}`, { cause: response });
}

let type = optionsOverride.type || this.options.type;
let body = await this.getResponseValue(response, type);

let body;
if(typeof this.url === "object" && typeof this.url.then === "function") {
body = await this.url;
} else if (typeof this.url === "function" && this.url.constructor.name === "AsyncFunction") {
body = await this.url();
} else {
let response = await fetch(this.url, fetchOptions);
if (!response.ok) {
throw new Error(
`Bad response for ${this.displayUrl} (${response.status}): ${response.statusText}`,
{ cause: response }
);
}

body = await this.getResponseValue(response, type);
}
if(!isDryRun) {
await super.save(body, type);
}
Expand Down
19 changes: 19 additions & 0 deletions test/AssetCacheTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,23 @@ test("Cache path should handle slashes without creating directories, issue #14",
let cachePath = normalizePath(cache.cachePath);

t.is(cachePath, "/tmp/.cache/eleventy-fetch-135797dbf5ab1187e5003c49162602");
});

test("Uses formatUrlForDisplay when caching a promise", async t => {
let promise = Promise.resolve();
let displayUrl = 'mock-display-url'
let asset = new AssetCache(promise, ".customcache", {
formatUrlForDisplay() {
return displayUrl;
}
});
let cachePath = normalizePath(asset.cachePath);
let jsonCachePath = normalizePath(asset.getCachedContentsPath("json"));

await asset.save({name: "Sophia Smith" }, "json");

t.truthy(fs.existsSync(jsonCachePath));

fs.unlinkSync(cachePath);
fs.unlinkSync(jsonCachePath);
});
103 changes: 96 additions & 7 deletions test/RemoteAssetCacheTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ const { Util } = require("../");
const AssetCache = require("../src/AssetCache");
const RemoteAssetCache = require("../src/RemoteAssetCache");

// Cause is an optional enhancement for Node 16.19+
let [major, minor] = process.version.split(".");
major = parseInt(major.startsWith("v") ? major.slice(1) : major, 10);
minor = parseInt(minor, 10);
const isCauseSupported = major > 16 || major === 16 && minor > 19;

test("getDurationMs", t => {
let cache = new RemoteAssetCache("lksdjflkjsdf");
t.is(cache.getDurationMs("1s"), 1000);
Expand Down Expand Up @@ -112,13 +118,96 @@ test("Error with `cause`", async t => {
} catch(e) {
t.is(e.message, `Bad response for https://example.com/207115/photos/243-0-1.jpg (404): Not Found`)

// Cause is an optional enhancement for Node 16.19+
let [major, minor] = process.version.split(".");
major = parseInt(major.startsWith("v") ? major.slice(1) : major, 10);
minor = parseInt(minor, 10);

if(major > 16 || major === 16 && minor > 19) {
if(isCauseSupported) {
t.truthy(e.cause);
}
}
});
});

test("supports promises that resolve", async (t) => {
let expected = { mockKey: "mockValue" };
let promise = Promise.resolve(expected);
let asset = new RemoteAssetCache(promise, undefined, {
type: "json",
formatUrlForDisplay() {
return "resolve-promise";
},
});

let actual = await asset.fetch();

t.deepEqual(actual, expected);
});

test("supports promises that reject", async (t) => {
let expected = "mock error message";
let cause = new Error("mock cause");
let promise;
if (isCauseSupported) {
promise = Promise.reject(new Error(expected, { cause }));
} else {
promise = Promise.reject(new Error(expected));
}
let asset = new RemoteAssetCache(promise, undefined, {
formatUrlForDisplay() {
return "reject-promise";
},
});

try {
await asset.fetch();
} catch (e) {
t.is(e.message, expected);

if (isCauseSupported) {
t.is(e.cause, cause);
}
}
});

test("supports async functions that return data", async (t) => {
let expected = { mockKey: "mockValue" };
let asyncFunction = async () => {
return Promise.resolve(expected);
};
let asset = new RemoteAssetCache(asyncFunction, undefined, {
type: "json",
formatUrlForDisplay() {
return "async-return";
},
});

let actual = await asset.fetch();

t.deepEqual(actual, expected);
});

test("supports async functions that throw", async (t) => {
let expected = "mock error message";
let cause = new Error("mock cause");
let asyncFunction;
if (isCauseSupported) {
asyncFunction = async () => {
throw new Error(expected, { cause });
};
} else {
asyncFunction = async () => {
throw new Error(expected);
};
}
let asset = new RemoteAssetCache(asyncFunction, undefined, {
formatUrlForDisplay() {
return "async-throws";
},
});

try {
await asset.fetch();
} catch (e) {
t.is(e.message, expected);

if (isCauseSupported) {
t.is(e.cause, cause);
}
}
});

0 comments on commit 80673c8

Please sign in to comment.