Skip to content

Commit d77799d

Browse files
committed
feat: better cache invalidation
1 parent fa970af commit d77799d

File tree

2 files changed

+125
-155
lines changed

2 files changed

+125
-155
lines changed

packages/rpc/src/routers/websites.ts

Lines changed: 17 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,21 @@ import {
1717
getBillingCustomerId,
1818
trackWebsiteUsage,
1919
} from '../utils/billing';
20+
import {
21+
invalidateBasicWebsiteCaches,
22+
invalidateWebsiteCaches,
23+
} from '../utils/cache-invalidation';
2024

21-
// Cache configuration
2225
const websiteCache = createDrizzleCache({ redis, namespace: 'websites' });
2326
const CACHE_DURATION = 60; // seconds
2427
const TREND_THRESHOLD = 5; // percentage
2528

26-
// Types
2729
interface ChartDataPoint {
2830
websiteId: string;
2931
date: string;
3032
value: number;
3133
}
3234

33-
// Helper functions
3435
const buildFullDomain = (domain: string, subdomain?: string) =>
3536
subdomain ? `${subdomain}.${domain}` : domain;
3637

@@ -148,7 +149,6 @@ const fetchChartData = async (
148149
return processedData;
149150
};
150151

151-
// Router definition
152152
export const websitesRouter = createTRPCRouter({
153153
list: protectedProcedure
154154
.input(z.object({ organizationId: z.string().optional() }).default({}))
@@ -217,7 +217,6 @@ export const websitesRouter = createTRPCRouter({
217217
create: protectedProcedure
218218
.input(createWebsiteSchema)
219219
.mutation(async ({ ctx, input }) => {
220-
// Validate organization permissions upfront
221220
if (input.organizationId) {
222221
const { success } = await websitesApi.hasPermission({
223222
headers: ctx.headers,
@@ -236,7 +235,6 @@ export const websitesRouter = createTRPCRouter({
236235
input.organizationId
237236
);
238237

239-
// Check billing limits before starting transaction
240238
const creationLimitCheck =
241239
await checkAndTrackWebsiteCreation(billingCustomerId);
242240
if (!creationLimitCheck.allowed) {
@@ -252,9 +250,7 @@ export const websitesRouter = createTRPCRouter({
252250
buildWebsiteFilter(ctx.user.id, input.organizationId)
253251
);
254252

255-
// Execute database operations in transaction
256253
const createdWebsite = await ctx.db.transaction(async (tx) => {
257-
// Check for duplicate websites within transaction
258254
const duplicateWebsite = await tx.query.websites.findFirst({
259255
where: websiteFilter,
260256
});
@@ -285,7 +281,6 @@ export const websitesRouter = createTRPCRouter({
285281
return website;
286282
});
287283

288-
// Log success after transaction completes
289284
logger.success(
290285
'Website Created',
291286
`New website "${createdWebsite.name}" was created with domain "${createdWebsite.domain}"`,
@@ -297,23 +292,20 @@ export const websitesRouter = createTRPCRouter({
297292
}
298293
);
299294

300-
// Invalidate cache after successful creation
301-
await websiteCache.invalidateByTables(['websites']);
295+
await invalidateBasicWebsiteCaches(createdWebsite.id, websiteCache);
302296

303297
return createdWebsite;
304298
}),
305299

306300
update: protectedProcedure
307301
.input(updateWebsiteSchema)
308302
.mutation(async ({ ctx, input }) => {
309-
// Authorize access and get billing info before transaction
310303
const websiteToUpdate = await authorizeWebsiteAccess(
311304
ctx,
312305
input.id,
313306
'update'
314307
);
315308

316-
// Execute update in transaction
317309
const updatedWebsite = await ctx.db.transaction(async (tx) => {
318310
const [website] = await tx
319311
.update(websites)
@@ -331,7 +323,6 @@ export const websitesRouter = createTRPCRouter({
331323
return website;
332324
});
333325

334-
// Log success after transaction
335326
logger.info(
336327
'Website Updated',
337328
`Website "${websiteToUpdate.name}" was renamed to "${updatedWebsite.name}"`,
@@ -343,71 +334,17 @@ export const websitesRouter = createTRPCRouter({
343334
}
344335
);
345336

346-
// Invalidate cache after successful update
347-
await Promise.all([
348-
websiteCache.invalidateByTables(['websites']),
349-
websiteCache.invalidateByKey(`getById:${input.id}`),
350-
]);
337+
await invalidateBasicWebsiteCaches(input.id, websiteCache);
351338

352-
// If isPublic status changed, invalidate all related caches
353339
if (
354340
input.isPublic !== undefined &&
355341
input.isPublic !== websiteToUpdate.isPublic
356342
) {
357-
// Call the invalidateCaches procedure logic directly
358-
await Promise.all([
359-
// Website caches
360-
websiteCache.invalidateByTables(['websites']),
361-
websiteCache.invalidateByKey(`getById:${input.id}`),
362-
363-
createDrizzleCache({
364-
redis,
365-
namespace: 'website_by_id',
366-
}).invalidateByKey(`website_by_id:${input.id}`),
367-
createDrizzleCache({ redis, namespace: 'auth' }).invalidateByKey(
368-
`auth:${ctx.user.id}:${input.id}`
369-
),
370-
371-
// Funnel caches
372-
createDrizzleCache({
373-
redis,
374-
namespace: 'funnels',
375-
}).invalidateByTables(['funnelDefinitions']),
376-
createDrizzleCache({ redis, namespace: 'funnels' }).invalidateByKey(
377-
`funnels:list:${input.id}`
378-
),
379-
createDrizzleCache({ redis, namespace: 'funnels' }).invalidateByKey(
380-
`funnels:listPublic:${input.id}`
381-
),
382-
383-
// Goals caches
384-
createDrizzleCache({ redis, namespace: 'goals' }).invalidateByTables([
385-
'goals',
386-
]),
387-
createDrizzleCache({ redis, namespace: 'goals' }).invalidateByKey(
388-
`goals:list:${input.id}`
389-
),
390-
391-
// Autocomplete caches
392-
createDrizzleCache({
393-
redis,
394-
namespace: 'autocomplete',
395-
}).invalidateByTables(['websites']),
396-
397-
// Mini-charts caches
398-
createDrizzleCache({
399-
redis,
400-
namespace: 'mini-charts',
401-
}).invalidateByTables(['websites']),
402-
createDrizzleCache({
403-
redis,
404-
namespace: 'mini-charts',
405-
}).invalidateByKey(`mini-charts:${ctx.user.id}:${input.id}`),
406-
createDrizzleCache({
407-
redis,
408-
namespace: 'mini-charts',
409-
}).invalidateByKey(`mini-charts:public:${input.id}`),
410-
]);
343+
await invalidateWebsiteCaches(
344+
input.id,
345+
ctx.user.id,
346+
`public status changed to ${input.isPublic}`
347+
);
411348

412349
logger.info(
413350
'Public status changed - caches invalidated',
@@ -427,7 +364,6 @@ export const websitesRouter = createTRPCRouter({
427364
delete: protectedProcedure
428365
.input(z.object({ id: z.string() }))
429366
.mutation(async ({ ctx, input }) => {
430-
// Authorize access and get billing info before transaction
431367
const websiteToDelete = await authorizeWebsiteAccess(
432368
ctx,
433369
input.id,
@@ -438,16 +374,13 @@ export const websitesRouter = createTRPCRouter({
438374
websiteToDelete.organizationId
439375
);
440376

441-
// Execute deletion and billing update in transaction
442377
await ctx.db.transaction(async (tx) => {
443-
// Delete website
444378
await tx.delete(websites).where(eq(websites.id, input.id));
445379

446380
// Track billing usage (decrement)
447381
await trackWebsiteUsage(billingCustomerId, -1);
448382
});
449383

450-
// Log after successful deletion
451384
logger.warning(
452385
'Website Deleted',
453386
`Website "${websiteToDelete.name}" with domain "${websiteToDelete.domain}" was deleted`,
@@ -459,22 +392,16 @@ export const websitesRouter = createTRPCRouter({
459392
}
460393
);
461394

462-
// Invalidate cache after successful deletion
463-
await Promise.all([
464-
websiteCache.invalidateByTables(['websites']),
465-
websiteCache.invalidateByKey(`getById:${input.id}`),
466-
]);
395+
await invalidateBasicWebsiteCaches(input.id, websiteCache);
467396

468397
return { success: true };
469398
}),
470399

471400
transfer: protectedProcedure
472401
.input(transferWebsiteSchema)
473402
.mutation(async ({ ctx, input }) => {
474-
// Authorize access before transaction
475403
await authorizeWebsiteAccess(ctx, input.websiteId, 'update');
476404

477-
// Validate organization permissions upfront
478405
if (input.organizationId) {
479406
const { success } = await websitesApi.hasPermission({
480407
headers: ctx.headers,
@@ -488,7 +415,6 @@ export const websitesRouter = createTRPCRouter({
488415
}
489416
}
490417

491-
// Execute transfer in transaction
492418
const transferredWebsite = await ctx.db.transaction(async (tx) => {
493419
const [website] = await tx
494420
.update(websites)
@@ -502,7 +428,6 @@ export const websitesRouter = createTRPCRouter({
502428
return website;
503429
});
504430

505-
// Log success after transaction
506431
logger.info(
507432
'Website Transferred',
508433
`Website "${transferredWebsite.name}" was transferred to organization "${input.organizationId}"`,
@@ -513,84 +438,21 @@ export const websitesRouter = createTRPCRouter({
513438
}
514439
);
515440

516-
// Invalidate cache after successful transfer
517-
await Promise.all([
518-
websiteCache.invalidateByTables(['websites']),
519-
websiteCache.invalidateByKey(`getById:${input.websiteId}`),
520-
]);
441+
await invalidateBasicWebsiteCaches(input.websiteId, websiteCache);
521442

522443
return transferredWebsite;
523444
}),
524445

525446
invalidateCaches: protectedProcedure
526447
.input(z.object({ websiteId: z.string() }))
527448
.mutation(async ({ ctx, input }) => {
528-
// Authorize access
529449
await authorizeWebsiteAccess(ctx, input.websiteId, 'update');
530450

531451
try {
532-
// Invalidate all caches related to this website
533-
await Promise.all([
534-
// Website caches
535-
websiteCache.invalidateByTables(['websites']),
536-
websiteCache.invalidateByKey(`getById:${input.websiteId}`),
537-
538-
createDrizzleCache({
539-
redis,
540-
namespace: 'website_by_id',
541-
}).invalidateByKey(`website_by_id:${input.websiteId}`),
542-
createDrizzleCache({ redis, namespace: 'auth' }).invalidateByKey(
543-
`auth:${ctx.user.id}:${input.websiteId}`
544-
),
545-
546-
// Funnel caches
547-
createDrizzleCache({
548-
redis,
549-
namespace: 'funnels',
550-
}).invalidateByTables(['funnelDefinitions']),
551-
createDrizzleCache({ redis, namespace: 'funnels' }).invalidateByKey(
552-
`funnels:list:${input.websiteId}`
553-
),
554-
createDrizzleCache({ redis, namespace: 'funnels' }).invalidateByKey(
555-
`funnels:listPublic:${input.websiteId}`
556-
),
557-
558-
// Goals caches
559-
createDrizzleCache({ redis, namespace: 'goals' }).invalidateByTables([
560-
'goals',
561-
]),
562-
createDrizzleCache({ redis, namespace: 'goals' }).invalidateByKey(
563-
`goals:list:${input.websiteId}`
564-
),
565-
566-
// Autocomplete caches
567-
createDrizzleCache({
568-
redis,
569-
namespace: 'autocomplete',
570-
}).invalidateByTables(['websites']),
571-
572-
// Mini-charts caches
573-
createDrizzleCache({
574-
redis,
575-
namespace: 'mini-charts',
576-
}).invalidateByTables(['websites']),
577-
createDrizzleCache({
578-
redis,
579-
namespace: 'mini-charts',
580-
}).invalidateByKey(`mini-charts:${ctx.user.id}:${input.websiteId}`),
581-
createDrizzleCache({
582-
redis,
583-
namespace: 'mini-charts',
584-
}).invalidateByKey(`mini-charts:public:${input.websiteId}`),
585-
]);
586-
587-
logger.info(
588-
'Caches invalidated',
589-
`All caches invalidated for website ${input.websiteId}`,
590-
{
591-
websiteId: input.websiteId,
592-
userId: ctx.user.id,
593-
}
452+
await invalidateWebsiteCaches(
453+
input.websiteId,
454+
ctx.user.id,
455+
'manual cache invalidation'
594456
);
595457

596458
return { success: true };

0 commit comments

Comments
 (0)