mirror of
https://github.com/svemagie/blog-eleventy-indiekit.git
synced 2026-04-02 08:44:56 +02:00
perf: add timeout and watch-mode cache extension to all data files
Introduce shared cachedFetch helper (lib/data-fetch.js) wrapping EleventyFetch with two protections: - 10-second hard timeout via AbortController on every network request, preventing slow or unresponsive APIs from hanging the build - 4-hour cache TTL in watch/serve mode (vs 5-15 min originals), so incremental rebuilds serve from disk cache instead of re-fetching APIs every time a markdown file changes All 13 network _data files updated to use cachedFetch. Production builds keep original short TTLs for fresh data. Targets the "Data File" benchmark (12,169ms / 32% of incremental rebuild) — the largest remaining bottleneck after filter memoization. Confab-Link: http://localhost:8080/sessions/0b241cd6-aff2-4fec-853c-2b5a61e61946
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* Used for conditional navigation — the blogroll page itself loads data client-side.
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
|
||||
@@ -12,7 +12,7 @@ export default async function () {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/blogrollapi/api/status`;
|
||||
console.log(`[blogrollStatus] Checking API: ${url}`);
|
||||
const data = await EleventyFetch(url, {
|
||||
const data = await cachedFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Fetches recent posts from Bluesky using the AT Protocol API
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
import { BskyAgent } from "@atproto/api";
|
||||
|
||||
export default async function () {
|
||||
@@ -16,7 +16,7 @@ export default async function () {
|
||||
// Get the author's feed using public API (no auth needed for public posts)
|
||||
const feedUrl = `https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${handle}&limit=10`;
|
||||
|
||||
const response = await EleventyFetch(feedUrl, {
|
||||
const response = await cachedFetch(feedUrl, {
|
||||
duration: "15m", // Cache for 15 minutes
|
||||
type: "json",
|
||||
fetchOptions: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
export default async function () {
|
||||
try {
|
||||
const data = await EleventyFetch(
|
||||
const data = await cachedFetch(
|
||||
"http://127.0.0.1:8080/conversations/api/mentions?per-page=10000",
|
||||
{ duration: "15m", type: "json" }
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Fetches from Indiekit's endpoint-funkwhale public API
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
const FUNKWHALE_INSTANCE = process.env.FUNKWHALE_INSTANCE || "";
|
||||
@@ -15,7 +15,7 @@ async function fetchFromIndiekit(endpoint) {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/funkwhaleapi/api/${endpoint}`;
|
||||
console.log(`[funkwhaleActivity] Fetching from Indiekit: ${url}`);
|
||||
const data = await EleventyFetch(url, {
|
||||
const data = await cachedFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Falls back to direct GitHub API if Indiekit is unavailable
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
const GITHUB_USERNAME = process.env.GITHUB_USERNAME || "";
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
@@ -19,7 +19,7 @@ async function fetchFromIndiekit(endpoint) {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/githubapi/api/${endpoint}`;
|
||||
console.log(`[githubActivity] Fetching from Indiekit: ${url}`);
|
||||
const data = await EleventyFetch(url, {
|
||||
const data = await cachedFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
});
|
||||
@@ -47,7 +47,7 @@ async function fetchFromGitHub(endpoint) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
return await EleventyFetch(url, {
|
||||
return await cachedFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
fetchOptions: { headers },
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Fetches public repositories from GitHub API
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
export default async function () {
|
||||
const username = process.env.GITHUB_USERNAME || "";
|
||||
@@ -12,7 +12,7 @@ export default async function () {
|
||||
// Fetch public repos, sorted by updated date
|
||||
const url = `https://api.github.com/users/${username}/repos?sort=updated&per_page=10&type=owner`;
|
||||
|
||||
const repos = await EleventyFetch(url, {
|
||||
const repos = await cachedFetch(url, {
|
||||
duration: "1h", // Cache for 1 hour
|
||||
type: "json",
|
||||
fetchOptions: {
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
* The starred page fetches all data client-side via Alpine.js.
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
|
||||
export default async function () {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/githubapi/api/starred/all`;
|
||||
const response = await EleventyFetch(url, {
|
||||
const response = await cachedFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Fetches from Indiekit's endpoint-lastfm public API
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
const LASTFM_USERNAME = process.env.LASTFM_USERNAME || "";
|
||||
@@ -15,7 +15,7 @@ async function fetchFromIndiekit(endpoint) {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/lastfmapi/api/${endpoint}`;
|
||||
console.log(`[lastfmActivity] Fetching from Indiekit: ${url}`);
|
||||
const data = await EleventyFetch(url, {
|
||||
const data = await cachedFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Fetches recent posts from Mastodon using the public API
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
export default async function () {
|
||||
const instance = process.env.MASTODON_INSTANCE?.replace("https://", "") || "";
|
||||
@@ -13,7 +13,7 @@ export default async function () {
|
||||
// First, look up the account ID
|
||||
const lookupUrl = `https://${instance}/api/v1/accounts/lookup?acct=${username}`;
|
||||
|
||||
const account = await EleventyFetch(lookupUrl, {
|
||||
const account = await cachedFetch(lookupUrl, {
|
||||
duration: "1h", // Cache account lookup for 1 hour
|
||||
type: "json",
|
||||
fetchOptions: {
|
||||
@@ -31,7 +31,7 @@ export default async function () {
|
||||
// Fetch recent statuses (excluding replies and boosts for cleaner feed)
|
||||
const statusesUrl = `https://${instance}/api/v1/accounts/${account.id}/statuses?limit=10&exclude_replies=true&exclude_reblogs=true`;
|
||||
|
||||
const statuses = await EleventyFetch(statusesUrl, {
|
||||
const statuses = await cachedFetch(statusesUrl, {
|
||||
duration: "15m", // Cache for 15 minutes
|
||||
type: "json",
|
||||
fetchOptions: {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Fetches from Indiekit's endpoint-rss public API
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
|
||||
@@ -14,7 +14,7 @@ async function fetchFromIndiekit(endpoint) {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/rssapi/api/${endpoint}`;
|
||||
console.log(`[newsActivity] Fetching from Indiekit: ${url}`);
|
||||
const data = await EleventyFetch(url, {
|
||||
const data = await cachedFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Used for conditional navigation — the podroll page itself loads data client-side.
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
|
||||
@@ -12,7 +12,7 @@ export default async function () {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/podrollapi/api/status`;
|
||||
console.log(`[podrollStatus] Checking API: ${url}`);
|
||||
const data = await EleventyFetch(url, {
|
||||
const data = await cachedFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Fetches the 5 most recent comments at build time for the sidebar widget.
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
|
||||
@@ -11,7 +11,7 @@ export default async function () {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/comments/api/comments?limit=5`;
|
||||
console.log(`[recentComments] Fetching: ${url}`);
|
||||
const data = await EleventyFetch(url, {
|
||||
const data = await cachedFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Supports single or multiple channels
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
import { cachedFetch } from "../lib/data-fetch.js";
|
||||
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
|
||||
@@ -15,7 +15,7 @@ async function fetchFromIndiekit(endpoint) {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/youtubeapi/api/${endpoint}`;
|
||||
console.log(`[youtubeChannel] Fetching from Indiekit: ${url}`);
|
||||
const data = await EleventyFetch(url, {
|
||||
const data = await cachedFetch(url, {
|
||||
duration: "5m",
|
||||
type: "json",
|
||||
});
|
||||
|
||||
54
lib/data-fetch.js
Normal file
54
lib/data-fetch.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Shared data-fetching helper for _data files.
|
||||
*
|
||||
* Wraps @11ty/eleventy-fetch with two protections:
|
||||
* 1. Hard timeout — 10-second AbortController ceiling on every request
|
||||
* 2. Watch-mode cache extension — uses "4h" TTL during watch/serve,
|
||||
* keeping the original (shorter) TTL only for production builds
|
||||
*
|
||||
* Usage:
|
||||
* import { cachedFetch } from "../lib/data-fetch.js";
|
||||
* const data = await cachedFetch(url, { duration: "15m", type: "json" });
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
|
||||
const FETCH_TIMEOUT_MS = 10_000; // 10 seconds
|
||||
|
||||
// In watch/serve mode, extend cache to avoid re-fetching on every rebuild.
|
||||
// Production builds use the caller's original TTL for fresh data.
|
||||
const isWatchMode = process.env.ELEVENTY_RUN_MODE !== "build";
|
||||
const WATCH_MODE_DURATION = "4h";
|
||||
|
||||
/**
|
||||
* Fetch with timeout and watch-mode cache extension.
|
||||
*
|
||||
* @param {string} url - URL to fetch
|
||||
* @param {object} options - EleventyFetch options (duration, type, fetchOptions, etc.)
|
||||
* @returns {Promise<any>} Parsed response
|
||||
*/
|
||||
export async function cachedFetch(url, options = {}) {
|
||||
// Extend cache in watch mode
|
||||
const duration = isWatchMode ? WATCH_MODE_DURATION : (options.duration || "15m");
|
||||
|
||||
// Create abort controller for hard timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const fetchOptions = {
|
||||
...options.fetchOptions,
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
const result = await EleventyFetch(url, {
|
||||
...options,
|
||||
duration,
|
||||
fetchOptions,
|
||||
});
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user