perf(slideshow): cache global settings to cut /state DB reads (PR #646 review)
Each /state poll fired ~10 getAppSetting reads to resolve the watermark/fit; a leaked link x N tabs amplified that linearly (review concern 2). Add a 5s-TTL cached bundle (utils/slideshowGlobals) for the global slideshow_* + branding-logo settings, invalidated on PUT /admin/settings/slideshow so admin live-edit stays instant. slideshowSettings now does ~2 reads per poll (event row + photo count) on a cache hit. Also documents the frontend optimistic-default nit.
This commit is contained in:
@@ -25,6 +25,7 @@ const cookieParser = require('cookie-parser');
|
||||
const request = require('supertest');
|
||||
const { bootCrmDb, seedMinimal } = require('../integration/helpers/crmDb');
|
||||
const { invalidateFeatureFlagCache } = require('../../src/middleware/requireFeatureFlag');
|
||||
const { invalidateSlideshowGlobals } = require('../../src/utils/slideshowGlobals');
|
||||
|
||||
const SLUG = 'wedding-test';
|
||||
const TOKEN = 'show-tok-abcdef';
|
||||
@@ -89,6 +90,7 @@ describe('public Live Slideshow routes', () => {
|
||||
await db('app_settings').del();
|
||||
await db('feature_flags').del();
|
||||
invalidateFeatureFlagCache();
|
||||
invalidateSlideshowGlobals();
|
||||
await setFlag(db, 'slideshow', true);
|
||||
});
|
||||
|
||||
|
||||
@@ -345,6 +345,9 @@ router.put('/slideshow', adminAuth, requirePermission('settings.edit'), async (r
|
||||
for (const u of updates) {
|
||||
await upsertAppSetting(u.setting_key, u.setting_value, u.setting_type);
|
||||
}
|
||||
// Drop the slideshow-globals cache so a running projector picks up the
|
||||
// change on its next poll rather than after the 5s TTL.
|
||||
require('../utils/slideshowGlobals').invalidateSlideshowGlobals();
|
||||
res.json({ message: 'Slideshow settings updated', updated: updates.map((u) => u.setting_key) });
|
||||
} catch (error) {
|
||||
console.error('Slideshow settings save error:', error);
|
||||
|
||||
@@ -28,7 +28,7 @@ const { getStorage } = require('../services/storage');
|
||||
const { setGalleryAuthCookies } = require('../utils/tokenUtils');
|
||||
// Read globals from app_settings (the real table) — settingsService.getSetting
|
||||
// queries a non-existent `settings` table and throws.
|
||||
const { getAppSetting } = require('../utils/appSettings');
|
||||
const { getSlideshowGlobals } = require('../utils/slideshowGlobals');
|
||||
const { isFeatureEnabled } = require('../middleware/requireFeatureFlag');
|
||||
const fs = require('fs');
|
||||
|
||||
@@ -269,48 +269,48 @@ async function resolveSlideshow(slug, token) {
|
||||
// from the chosen source so the kiosk renders it without knowing about
|
||||
// branding/event internals; null url = nothing to overlay.
|
||||
async function slideshowSettings(event) {
|
||||
// The global look/fit (Settings → Slideshow) + branding logo URLs come from a
|
||||
// short-TTL cached bundle so a 3s projector poll doesn't re-fire ~10 settings
|
||||
// reads each time (PR #646 review, concern 2).
|
||||
const g = await getSlideshowGlobals();
|
||||
|
||||
// Watermark: the LOOK (logo/position/opacity/style/size) is configured ONCE
|
||||
// globally (Settings → Slideshow); it is NOT duplicated per event. The only
|
||||
// per-event control is whether the watermark shows: `show_watermark` NULL
|
||||
// inherits the global enabled flag, true/false force it on/off.
|
||||
// globally; it is NOT duplicated per event. The only per-event control is
|
||||
// whether the watermark shows: `show_watermark` NULL inherits the global
|
||||
// enabled flag, true/false force it on/off.
|
||||
const wm = event.show_watermark;
|
||||
const inherit = (wm === null || wm === undefined);
|
||||
const enabled = inherit
|
||||
? (await getAppSetting('slideshow_watermark_enabled', false)) === true
|
||||
: (wm === true || wm === 1 || wm === '1');
|
||||
const enabled = inherit ? g.watermark_enabled : (wm === true || wm === 1 || wm === '1');
|
||||
let watermark = null;
|
||||
if (enabled) {
|
||||
const source = await getAppSetting('slideshow_watermark_source', 'logo');
|
||||
const position = await getAppSetting('slideshow_watermark_position', 'bottom-right');
|
||||
const opacity = await getAppSetting('slideshow_watermark_opacity', 60);
|
||||
const style = await getAppSetting('slideshow_watermark_style', 'white');
|
||||
const size = await getAppSetting('slideshow_watermark_size', 12);
|
||||
// Resolve the chosen logo to a URL. Branding assets come from settings;
|
||||
// the event source uses the event's own hero logo.
|
||||
let url;
|
||||
if (source === 'event') {
|
||||
if (g.watermark_source === 'event') {
|
||||
url = event.hero_logo_url || null;
|
||||
} else if (source === 'logo_dark') {
|
||||
url = await getAppSetting('branding_logo_url_dark', null);
|
||||
} else if (source === 'favicon') {
|
||||
url = await getAppSetting('branding_favicon_url', null);
|
||||
} else if (g.watermark_source === 'logo_dark') {
|
||||
url = g.branding_logo_url_dark;
|
||||
} else if (g.watermark_source === 'favicon') {
|
||||
url = g.branding_favicon_url;
|
||||
} else {
|
||||
url = await getAppSetting('branding_logo_url', null);
|
||||
url = g.branding_logo_url;
|
||||
}
|
||||
if (url) {
|
||||
watermark = { url, position: position || 'bottom-right', opacity: opacity ?? 60, style: style || 'white', size: size ?? 12 };
|
||||
watermark = {
|
||||
url,
|
||||
position: g.watermark_position,
|
||||
opacity: g.watermark_opacity,
|
||||
style: g.watermark_style,
|
||||
size: g.watermark_size,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Image fit is a global setting (Settings → Slideshow): 'cover' fills + crops,
|
||||
// 'contain' shows the whole image with black bars (no crop).
|
||||
const fitRaw = await getAppSetting('slideshow_fit', 'cover');
|
||||
const fit = fitRaw === 'contain' ? 'contain' : 'cover';
|
||||
return {
|
||||
interval_ms: event.show_interval_ms || 5000,
|
||||
transition: event.show_transition || 'crossfade',
|
||||
transition_ms: event.show_transition_ms || 800,
|
||||
colorfilter: event.show_colorfilter || 'none',
|
||||
fit,
|
||||
fit: g.fit,
|
||||
watermark,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Cached read of the global Live-Slideshow settings (Settings → Slideshow) +
|
||||
* the branding logo URLs the watermark resolves against.
|
||||
*
|
||||
* Why: a running projector polls `/show/:token/state` every ~3s, and each poll
|
||||
* resolved the watermark/fit by firing ~7–10 individual `getAppSetting` reads.
|
||||
* A leaked link × N tabs amplifies that linearly (PR #646 review, concern 2).
|
||||
* These globals change only via `PUT /admin/settings/slideshow`, so we cache
|
||||
* the whole bundle with a short TTL and invalidate on write — admin live-edit
|
||||
* stays effectively instant, and steady-state polls drop to ~0 settings reads.
|
||||
*/
|
||||
const { getAppSetting } = require('./appSettings');
|
||||
|
||||
const TTL_MS = 5000;
|
||||
let cache = null; // { at, val }
|
||||
|
||||
async function getSlideshowGlobals() {
|
||||
const now = Date.now();
|
||||
if (cache && now - cache.at < TTL_MS) return cache.val;
|
||||
|
||||
const [
|
||||
enabled, source, position, opacity, style, size, fit,
|
||||
logo, logoDark, favicon,
|
||||
] = await Promise.all([
|
||||
getAppSetting('slideshow_watermark_enabled', false),
|
||||
getAppSetting('slideshow_watermark_source', 'logo'),
|
||||
getAppSetting('slideshow_watermark_position', 'bottom-right'),
|
||||
getAppSetting('slideshow_watermark_opacity', 60),
|
||||
getAppSetting('slideshow_watermark_style', 'white'),
|
||||
getAppSetting('slideshow_watermark_size', 12),
|
||||
getAppSetting('slideshow_fit', 'cover'),
|
||||
getAppSetting('branding_logo_url', null),
|
||||
getAppSetting('branding_logo_url_dark', null),
|
||||
getAppSetting('branding_favicon_url', null),
|
||||
]);
|
||||
|
||||
const val = {
|
||||
watermark_enabled: enabled === true,
|
||||
watermark_source: source || 'logo',
|
||||
watermark_position: position || 'bottom-right',
|
||||
watermark_opacity: opacity ?? 60,
|
||||
watermark_style: style || 'white',
|
||||
watermark_size: size ?? 12,
|
||||
fit: fit === 'contain' ? 'contain' : 'cover',
|
||||
branding_logo_url: logo || null,
|
||||
branding_logo_url_dark: logoDark || null,
|
||||
branding_favicon_url: favicon || null,
|
||||
};
|
||||
cache = { at: now, val };
|
||||
return val;
|
||||
}
|
||||
|
||||
/** Clear the cache — call after any write to the slideshow_* / branding logo settings. */
|
||||
function invalidateSlideshowGlobals() {
|
||||
cache = null;
|
||||
}
|
||||
|
||||
module.exports = { getSlideshowGlobals, invalidateSlideshowGlobals };
|
||||
Reference in New Issue
Block a user