diff --git a/.changeset/peek-site-setting.md b/.changeset/peek-site-setting.md new file mode 100644 index 0000000000..a93e239e7a --- /dev/null +++ b/.changeset/peek-site-setting.md @@ -0,0 +1,9 @@ +--- +"emdash": patch +--- + +`getSiteSetting(key)` now transparently piggybacks on `getSiteSettings()` when the batch has already been loaded in the current request. If a parent template has called `getSiteSettings()` (which is request-cached), a later `getSiteSetting("seo")` — from `EmDashHead`, a plugin, or user code — reads the key from that cached result instead of firing its own round-trip. Falls back to a per-key cached query when nothing has been primed. + +Exposes `peekRequestCache(key)` for internal use by other helpers that want the same "read from a broader cached query if available" pattern. + +On the blog-demo fixture: the SEO call added in PR #613 now costs zero extra queries per page (it reads from the Base layout's existing `getSiteSettings()` result). diff --git a/packages/core/src/components/EmDashHead.astro b/packages/core/src/components/EmDashHead.astro index 8c74016003..fb42b6b39e 100644 --- a/packages/core/src/components/EmDashHead.astro +++ b/packages/core/src/components/EmDashHead.astro @@ -37,10 +37,16 @@ let metadataHtml = ""; let fragmentsHtml = ""; if (runtime) { - // Run independent async loads in parallel: site SEO settings (for search - // engine verification meta tags) and plugin page-metadata contributions. - // Plugin contributions come BEFORE site/base in the array, so - // resolvePageMetadata's first-wins dedup lets plugins override defaults. + // Run independent async loads in parallel: site SEO settings (for + // search engine verification meta tags) and plugin page-metadata + // contributions. Plugin contributions come BEFORE site/base in the + // array, so resolvePageMetadata's first-wins dedup lets plugins + // override defaults. + // + // `getSiteSetting("seo")` is request-cached and — crucially — reads + // from `getSiteSettings()`'s cached batch when a parent template has + // already called it. So this is either a single-key query or free, + // not a second round-trip. const [seoSettings, pluginContributions, fragments] = await Promise.all([ getSiteSetting("seo"), runtime.collectPageMetadata(page), diff --git a/packages/core/src/request-cache.ts b/packages/core/src/request-cache.ts index a6ea2c6bf5..d304bef59a 100644 --- a/packages/core/src/request-cache.ts +++ b/packages/core/src/request-cache.ts @@ -59,6 +59,24 @@ export function requestCached(key: string, fn: () => Promise): Promise return promise; } +/** + * Look up an entry in the request-scoped cache without inserting one. + * + * Returns the in-flight or resolved promise if the key exists in the + * current request, otherwise `undefined`. Callers can use this to + * opportunistically satisfy a narrower query (e.g. `getSiteSetting("seo")`) + * from a broader one (`getSiteSettings()`) that's already been loaded + * by a parent template — avoiding a redundant round-trip. + * + * No-ops outside a request context. + */ +export function peekRequestCache(key: string): Promise | undefined { + const ctx = getRequestContext(); + if (!ctx) return undefined; + const cache = store.get(ctx); + return cache?.get(key) as Promise | undefined; +} + /** * Pre-populate the request-scoped cache with a resolved value. * diff --git a/packages/core/src/settings/index.ts b/packages/core/src/settings/index.ts index 4f15557b55..8ecc208517 100644 --- a/packages/core/src/settings/index.ts +++ b/packages/core/src/settings/index.ts @@ -11,7 +11,7 @@ import { MediaRepository } from "../database/repositories/media.js"; import { OptionsRepository } from "../database/repositories/options.js"; import type { Database } from "../database/types.js"; import { getDb } from "../loader.js"; -import { requestCached } from "../request-cache.js"; +import { peekRequestCache, requestCached } from "../request-cache.js"; import type { Storage } from "../storage/types.js"; import type { SiteSettings, SiteSettingKey, MediaReference } from "./types.js"; @@ -73,13 +73,23 @@ async function resolveMediaReference( * console.log(logo?.url); // Resolved URL * ``` */ -export function getSiteSetting( +export async function getSiteSetting( key: K, ): Promise { - // Cache per-key within a request. Without this, templates that pull - // several settings (and layout components that ask for logo/favicon/ - // title separately) each fire an options-table query — which is a - // real latency hit on regions far from the D1 primary (APS, APE). + // If `getSiteSettings()` has already been called in this request, + // read from that (request-cached) batch rather than firing a second + // options-table query. Common layout: a Base template pulls the + // whole settings object up-front, then `EmDashHead` or a plugin + // asks for one key — no reason the singular call should round-trip + // again. + const primed = peekRequestCache>("siteSettings"); + if (primed) { + const settings = await primed; + return settings[key]; + } + + // Otherwise cache per-key. Templates that pull several settings + // independently still share the in-flight query for each one. return requestCached(`siteSetting:${key}`, async () => { const db = await getDb(); return getSiteSettingWithDb(key, db);