From fb1c6d21df8e22349f9b9a70737f72f4e75caee8 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 19 Apr 2026 15:52:56 +0100 Subject: [PATCH 1/2] perf: getSiteSetting(key) piggybacks on cached getSiteSettings() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds peekRequestCache() to request-cache.ts so a narrower query can opportunistically satisfy itself from a broader one already loaded. getSiteSetting(key) now first peeks for the "siteSettings" batch result (populated by getSiteSettings()) and reads the requested key from there if present. Falls back to a per-key cached query if the batch hasn't been loaded. Net effect on the blog-demo layout: getSiteSettings() is already called by Base.astro, so the EmDashHead getSiteSetting("seo") call from PR #613 now costs zero extra queries instead of one primary-routed round-trip per render. Query-count snapshot is unchanged — the +1 queries PR #663 couldn't dedupe are now gone. --- packages/core/src/components/EmDashHead.astro | 14 ++++++++---- packages/core/src/request-cache.ts | 18 +++++++++++++++ packages/core/src/settings/index.ts | 22 ++++++++++++++----- 3 files changed, 44 insertions(+), 10 deletions(-) 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); From fc1e988828135ce9179a75c3221c714d8ac35c1b Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 19 Apr 2026 15:56:18 +0100 Subject: [PATCH 2/2] add changeset --- .changeset/peek-site-setting.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/peek-site-setting.md 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).