Skip to content

Commit 43e43c8

Browse files
dahliaclaude
andcommitted
Optimize invitation system performance with caching
- Add caching for availableLocales query to avoid filesystem operations on every request - Add error handling for invalid locale tags in misc.ts - Implement email template caching to reduce disk I/O overhead - Preload all locale templates at startup for better performance Co-Authored-By: Claude <[email protected]>
1 parent f3f33c8 commit 43e43c8

File tree

2 files changed

+63
-14
lines changed

2 files changed

+63
-14
lines changed

graphql/invite.ts

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ InvitationRef.implement({
6969
});
7070

7171
const InviteInviterError = builder.enumType("InviteInviterError", {
72-
values: ["INVITER_NOT_AUTHENTICATED", "INVITER_NO_INVITATIONS_LEFT"] as const,
72+
values: ["INVITER_NOT_AUTHENTICATED", "INVITER_NO_INVITATIONS_LEFT", "INVITER_EMAIL_SEND_FAILED"] as const,
7373
});
7474

7575
const InviteEmailError = builder.enumType("InviteEmailError", {
@@ -223,6 +223,15 @@ builder.mutationField("invite", (t) =>
223223
"Failed to send invitation email: {errors}",
224224
{ errors: receipt.errorMessages },
225225
);
226+
// Credit back the invitation on email send failure
227+
await ctx.db.update(accountTable).set({
228+
leftInvitations: sql`${accountTable.leftInvitations} + 1`,
229+
}).where(eq(accountTable.id, ctx.account.id));
230+
231+
// Return validation error to inform the user
232+
return {
233+
inviter: "INVITER_EMAIL_SEND_FAILED",
234+
} satisfies InviteValidationErrors;
226235
}
227236
return {
228237
inviterId: ctx.account.id,
@@ -235,32 +244,62 @@ builder.mutationField("invite", (t) =>
235244

236245
const LOCALES_DIR = join(import.meta.dirname!, "locales");
237246

238-
async function getEmailTemplate(
239-
locale: Intl.Locale,
240-
message: boolean,
241-
): Promise<{ subject: string; content: string }> {
247+
// Cache for email templates
248+
let cachedTemplates: Map<string, { subject: string; emailContent: string; emailContentWithMessage: string }> | null = null;
249+
let cachedAvailableLocales: Record<string, string> | null = null;
250+
251+
async function loadEmailTemplates(): Promise<void> {
252+
if (cachedTemplates && cachedAvailableLocales) return;
253+
242254
const availableLocales: Record<string, string> = {};
255+
const templates = new Map<string, { subject: string; emailContent: string; emailContentWithMessage: string }>();
256+
243257
const files = expandGlob(join(LOCALES_DIR, "*.json"), {
244258
includeDirs: false,
245259
});
260+
246261
for await (const file of files) {
247262
if (!file.isFile) continue;
248263
const match = file.name.match(/^(.+)\.json$/);
249264
if (match == null) continue;
250265
const localeName = match[1];
251266
availableLocales[localeName] = file.path;
267+
268+
try {
269+
const json = await Deno.readTextFile(file.path);
270+
const data = JSON.parse(json);
271+
templates.set(localeName, {
272+
subject: data.invite.emailSubject,
273+
emailContent: data.invite.emailContent,
274+
emailContentWithMessage: data.invite.emailContentWithMessage,
275+
});
276+
} catch (error) {
277+
console.warn(`Failed to load email template for locale ${localeName}:`, error);
278+
}
252279
}
280+
281+
cachedTemplates = templates;
282+
cachedAvailableLocales = availableLocales;
283+
}
284+
285+
async function getEmailTemplate(
286+
locale: Intl.Locale,
287+
message: boolean,
288+
): Promise<{ subject: string; content: string }> {
289+
await loadEmailTemplates();
290+
253291
const selectedLocale =
254-
negotiateLocale(locale, Object.keys(availableLocales)) ??
292+
negotiateLocale(locale, Object.keys(cachedAvailableLocales!)) ??
255293
new Intl.Locale("en");
256-
const path = availableLocales[selectedLocale.baseName];
257-
const json = await Deno.readTextFile(path);
258-
const data = JSON.parse(json);
294+
295+
const template = cachedTemplates!.get(selectedLocale.baseName);
296+
if (!template) {
297+
throw new Error(`No email template found for locale ${selectedLocale.baseName}`);
298+
}
299+
259300
return {
260-
subject: data.invite.emailSubject,
261-
content: message
262-
? data.invite.emailContentWithMessage
263-
: data.invite.emailContent,
301+
subject: template.subject,
302+
content: message ? template.emailContentWithMessage : template.emailContent,
264303
};
265304
}
266305

graphql/misc.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import { builder } from "./builder.ts";
44

55
const LOCALES_DIR = join(import.meta.dirname!, "locales");
66

7+
let cachedLocales: Intl.Locale[] | null = null;
8+
79
builder.queryField("availableLocales", (t) =>
810
t.field({
911
type: ["Locale"],
1012
async resolve(_root, _args, _ctx) {
13+
if (cachedLocales) return cachedLocales;
14+
1115
const availableLocales: Intl.Locale[] = [];
1216
const files = expandGlob(join(LOCALES_DIR, "*.json"), {
1317
includeDirs: false,
@@ -17,8 +21,14 @@ builder.queryField("availableLocales", (t) =>
1721
const match = file.name.match(/^(.+)\.json$/);
1822
if (match == null) continue;
1923
const localeName = match[1];
20-
availableLocales.push(new Intl.Locale(localeName));
24+
try {
25+
availableLocales.push(new Intl.Locale(localeName));
26+
} catch {
27+
// ignore invalid locale tags
28+
}
2129
}
30+
31+
cachedLocales = availableLocales;
2232
return availableLocales;
2333
},
2434
}));

0 commit comments

Comments
 (0)