Skip to content

Commit b89c87c

Browse files
committed
fix(webapp): review round two - flag caching and invite icon theming
- the root loader reads hasThemeSwitcher through a short process-level TTL cache instead of a per-request database query - the invite envelope icon follows the System-theme grayscale treatment
1 parent 7c1561f commit b89c87c

3 files changed

Lines changed: 29 additions & 4 deletions

File tree

apps/webapp/app/root.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
normalizeThemePreference,
2929
type ThemePreference,
3030
} from "~/utils/themePreference";
31-
import { flag } from "~/v3/featureFlags.server";
31+
import { cachedFlag } from "~/v3/featureFlags.server";
3232
import { getTimezonePreference } from "./services/preferences/uiPreferences.server";
3333
import { appEnvTitleTag } from "./utils";
3434

@@ -80,9 +80,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
8080

8181
const user = await getUser(request);
8282
// Theme switching is feature-flagged; while off, everyone stays on the
83-
// classic theme even if a preference was saved earlier.
83+
// classic theme even if a preference was saved earlier. Cached: the root
84+
// loader runs on every document request and client navigation.
8485
const showThemeSwitcher = user
85-
? await flag({ key: "hasThemeSwitcher", defaultValue: false })
86+
? await cachedFlag({ key: "hasThemeSwitcher", defaultValue: false })
8687
: false;
8788
// Logged-out pages (login, invites) always render the branded Classic look.
8889
const themePreference: ThemePreference = showThemeSwitcher

apps/webapp/app/routes/invites.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ export default function Page() {
140140
>
141141
<div>
142142
<FormTitle
143-
LeadingIcon={<EnvelopeIcon className="size-6 text-cyan-500" />}
143+
LeadingIcon={
144+
<EnvelopeIcon className="size-6 text-cyan-500 system:text-text-bright" />
145+
}
144146
className="mb-0 text-sky-500 system:text-text-bright"
145147
title={simplur`You have ${invites.length} new invitation[|s]`}
146148
/>

apps/webapp/app/v3/featureFlags.server.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,28 @@ export function makeFlag(_prisma: PrismaClientOrTransaction = prisma) {
5555
return flag;
5656
}
5757

58+
const cachedFlagStore = new Map<string, { value: unknown; expiresAt: number }>();
59+
60+
/**
61+
* flag() behind a short process-level TTL cache, for global flags read on hot
62+
* paths (e.g. the root loader) where a database round-trip per request is too
63+
* expensive. Flips propagate within ttlMs per process. Not for org- or
64+
* user-scoped decisions.
65+
*/
66+
export async function cachedFlag<T extends FeatureFlagKey>(
67+
opts: FlagsOptions<T> & { defaultValue: z.infer<(typeof FeatureFlagCatalog)[T]> },
68+
ttlMs = 30_000
69+
): Promise<z.infer<(typeof FeatureFlagCatalog)[T]>> {
70+
const hit = cachedFlagStore.get(opts.key);
71+
if (hit && hit.expiresAt > Date.now()) {
72+
return hit.value as z.infer<(typeof FeatureFlagCatalog)[T]>;
73+
}
74+
75+
const value = await flag(opts);
76+
cachedFlagStore.set(opts.key, { value, expiresAt: Date.now() + ttlMs });
77+
return value;
78+
}
79+
5880
export function makeSetFlag(_prisma: PrismaClientOrTransaction = prisma) {
5981
return async function setFlag<T extends FeatureFlagKey>(
6082
opts: FlagsOptions<T> & { value: z.infer<(typeof FeatureFlagCatalog)[T]> }

0 commit comments

Comments
 (0)