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.
|
* 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";
|
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export default async function () {
|
|||||||
try {
|
try {
|
||||||
const url = `${INDIEKIT_URL}/blogrollapi/api/status`;
|
const url = `${INDIEKIT_URL}/blogrollapi/api/status`;
|
||||||
console.log(`[blogrollStatus] Checking API: ${url}`);
|
console.log(`[blogrollStatus] Checking API: ${url}`);
|
||||||
const data = await EleventyFetch(url, {
|
const data = await cachedFetch(url, {
|
||||||
duration: "15m",
|
duration: "15m",
|
||||||
type: "json",
|
type: "json",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Fetches recent posts from Bluesky using the AT Protocol API
|
* 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";
|
import { BskyAgent } from "@atproto/api";
|
||||||
|
|
||||||
export default async function () {
|
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)
|
// 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 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
|
duration: "15m", // Cache for 15 minutes
|
||||||
type: "json",
|
type: "json",
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
import { cachedFetch } from "../lib/data-fetch.js";
|
||||||
|
|
||||||
export default async function () {
|
export default async function () {
|
||||||
try {
|
try {
|
||||||
const data = await EleventyFetch(
|
const data = await cachedFetch(
|
||||||
"http://127.0.0.1:8080/conversations/api/mentions?per-page=10000",
|
"http://127.0.0.1:8080/conversations/api/mentions?per-page=10000",
|
||||||
{ duration: "15m", type: "json" }
|
{ duration: "15m", type: "json" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Fetches from Indiekit's endpoint-funkwhale public API
|
* 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 INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||||
const FUNKWHALE_INSTANCE = process.env.FUNKWHALE_INSTANCE || "";
|
const FUNKWHALE_INSTANCE = process.env.FUNKWHALE_INSTANCE || "";
|
||||||
@@ -15,7 +15,7 @@ async function fetchFromIndiekit(endpoint) {
|
|||||||
try {
|
try {
|
||||||
const url = `${INDIEKIT_URL}/funkwhaleapi/api/${endpoint}`;
|
const url = `${INDIEKIT_URL}/funkwhaleapi/api/${endpoint}`;
|
||||||
console.log(`[funkwhaleActivity] Fetching from Indiekit: ${url}`);
|
console.log(`[funkwhaleActivity] Fetching from Indiekit: ${url}`);
|
||||||
const data = await EleventyFetch(url, {
|
const data = await cachedFetch(url, {
|
||||||
duration: "15m",
|
duration: "15m",
|
||||||
type: "json",
|
type: "json",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Falls back to direct GitHub API if Indiekit is unavailable
|
* 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 GITHUB_USERNAME = process.env.GITHUB_USERNAME || "";
|
||||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||||
@@ -19,7 +19,7 @@ async function fetchFromIndiekit(endpoint) {
|
|||||||
try {
|
try {
|
||||||
const url = `${INDIEKIT_URL}/githubapi/api/${endpoint}`;
|
const url = `${INDIEKIT_URL}/githubapi/api/${endpoint}`;
|
||||||
console.log(`[githubActivity] Fetching from Indiekit: ${url}`);
|
console.log(`[githubActivity] Fetching from Indiekit: ${url}`);
|
||||||
const data = await EleventyFetch(url, {
|
const data = await cachedFetch(url, {
|
||||||
duration: "15m",
|
duration: "15m",
|
||||||
type: "json",
|
type: "json",
|
||||||
});
|
});
|
||||||
@@ -47,7 +47,7 @@ async function fetchFromGitHub(endpoint) {
|
|||||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await EleventyFetch(url, {
|
return await cachedFetch(url, {
|
||||||
duration: "15m",
|
duration: "15m",
|
||||||
type: "json",
|
type: "json",
|
||||||
fetchOptions: { headers },
|
fetchOptions: { headers },
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Fetches public repositories from GitHub API
|
* Fetches public repositories from GitHub API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
import { cachedFetch } from "../lib/data-fetch.js";
|
||||||
|
|
||||||
export default async function () {
|
export default async function () {
|
||||||
const username = process.env.GITHUB_USERNAME || "";
|
const username = process.env.GITHUB_USERNAME || "";
|
||||||
@@ -12,7 +12,7 @@ export default async function () {
|
|||||||
// Fetch public repos, sorted by updated date
|
// Fetch public repos, sorted by updated date
|
||||||
const url = `https://api.github.com/users/${username}/repos?sort=updated&per_page=10&type=owner`;
|
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
|
duration: "1h", // Cache for 1 hour
|
||||||
type: "json",
|
type: "json",
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
* The starred page fetches all data client-side via Alpine.js.
|
* 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";
|
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||||
|
|
||||||
export default async function () {
|
export default async function () {
|
||||||
try {
|
try {
|
||||||
const url = `${INDIEKIT_URL}/githubapi/api/starred/all`;
|
const url = `${INDIEKIT_URL}/githubapi/api/starred/all`;
|
||||||
const response = await EleventyFetch(url, {
|
const response = await cachedFetch(url, {
|
||||||
duration: "15m",
|
duration: "15m",
|
||||||
type: "json",
|
type: "json",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Fetches from Indiekit's endpoint-lastfm public API
|
* 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 INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||||
const LASTFM_USERNAME = process.env.LASTFM_USERNAME || "";
|
const LASTFM_USERNAME = process.env.LASTFM_USERNAME || "";
|
||||||
@@ -15,7 +15,7 @@ async function fetchFromIndiekit(endpoint) {
|
|||||||
try {
|
try {
|
||||||
const url = `${INDIEKIT_URL}/lastfmapi/api/${endpoint}`;
|
const url = `${INDIEKIT_URL}/lastfmapi/api/${endpoint}`;
|
||||||
console.log(`[lastfmActivity] Fetching from Indiekit: ${url}`);
|
console.log(`[lastfmActivity] Fetching from Indiekit: ${url}`);
|
||||||
const data = await EleventyFetch(url, {
|
const data = await cachedFetch(url, {
|
||||||
duration: "15m",
|
duration: "15m",
|
||||||
type: "json",
|
type: "json",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Fetches recent posts from Mastodon using the public API
|
* 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 () {
|
export default async function () {
|
||||||
const instance = process.env.MASTODON_INSTANCE?.replace("https://", "") || "";
|
const instance = process.env.MASTODON_INSTANCE?.replace("https://", "") || "";
|
||||||
@@ -13,7 +13,7 @@ export default async function () {
|
|||||||
// First, look up the account ID
|
// First, look up the account ID
|
||||||
const lookupUrl = `https://${instance}/api/v1/accounts/lookup?acct=${username}`;
|
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
|
duration: "1h", // Cache account lookup for 1 hour
|
||||||
type: "json",
|
type: "json",
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
@@ -31,7 +31,7 @@ export default async function () {
|
|||||||
// Fetch recent statuses (excluding replies and boosts for cleaner feed)
|
// 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 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
|
duration: "15m", // Cache for 15 minutes
|
||||||
type: "json",
|
type: "json",
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Fetches from Indiekit's endpoint-rss public API
|
* 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";
|
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ async function fetchFromIndiekit(endpoint) {
|
|||||||
try {
|
try {
|
||||||
const url = `${INDIEKIT_URL}/rssapi/api/${endpoint}`;
|
const url = `${INDIEKIT_URL}/rssapi/api/${endpoint}`;
|
||||||
console.log(`[newsActivity] Fetching from Indiekit: ${url}`);
|
console.log(`[newsActivity] Fetching from Indiekit: ${url}`);
|
||||||
const data = await EleventyFetch(url, {
|
const data = await cachedFetch(url, {
|
||||||
duration: "15m",
|
duration: "15m",
|
||||||
type: "json",
|
type: "json",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Used for conditional navigation — the podroll page itself loads data client-side.
|
* 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";
|
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export default async function () {
|
|||||||
try {
|
try {
|
||||||
const url = `${INDIEKIT_URL}/podrollapi/api/status`;
|
const url = `${INDIEKIT_URL}/podrollapi/api/status`;
|
||||||
console.log(`[podrollStatus] Checking API: ${url}`);
|
console.log(`[podrollStatus] Checking API: ${url}`);
|
||||||
const data = await EleventyFetch(url, {
|
const data = await cachedFetch(url, {
|
||||||
duration: "15m",
|
duration: "15m",
|
||||||
type: "json",
|
type: "json",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Fetches the 5 most recent comments at build time for the sidebar widget.
|
* 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";
|
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export default async function () {
|
|||||||
try {
|
try {
|
||||||
const url = `${INDIEKIT_URL}/comments/api/comments?limit=5`;
|
const url = `${INDIEKIT_URL}/comments/api/comments?limit=5`;
|
||||||
console.log(`[recentComments] Fetching: ${url}`);
|
console.log(`[recentComments] Fetching: ${url}`);
|
||||||
const data = await EleventyFetch(url, {
|
const data = await cachedFetch(url, {
|
||||||
duration: "15m",
|
duration: "15m",
|
||||||
type: "json",
|
type: "json",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Supports single or multiple channels
|
* 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";
|
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ async function fetchFromIndiekit(endpoint) {
|
|||||||
try {
|
try {
|
||||||
const url = `${INDIEKIT_URL}/youtubeapi/api/${endpoint}`;
|
const url = `${INDIEKIT_URL}/youtubeapi/api/${endpoint}`;
|
||||||
console.log(`[youtubeChannel] Fetching from Indiekit: ${url}`);
|
console.log(`[youtubeChannel] Fetching from Indiekit: ${url}`);
|
||||||
const data = await EleventyFetch(url, {
|
const data = await cachedFetch(url, {
|
||||||
duration: "5m",
|
duration: "5m",
|
||||||
type: "json",
|
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