Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/peek-site-setting.md
Original file line number Diff line number Diff line change
@@ -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).
14 changes: 10 additions & 4 deletions packages/core/src/components/EmDashHead.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/request-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ export function requestCached<T>(key: string, fn: () => Promise<T>): Promise<T>
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<T>(key: string): Promise<T> | undefined {
const ctx = getRequestContext();
if (!ctx) return undefined;
const cache = store.get(ctx);
return cache?.get(key) as Promise<T> | undefined;
}

/**
* Pre-populate the request-scoped cache with a resolved value.
*
Expand Down
22 changes: 16 additions & 6 deletions packages/core/src/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -73,13 +73,23 @@ async function resolveMediaReference(
* console.log(logo?.url); // Resolved URL
* ```
*/
export function getSiteSetting<K extends SiteSettingKey>(
export async function getSiteSetting<K extends SiteSettingKey>(
key: K,
): Promise<SiteSettings[K] | undefined> {
// 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<Partial<SiteSettings>>("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);
Expand Down
Loading