All files / lib project-cache.ts

100% Statements 24/24
100% Branches 12/12
100% Functions 6/6
100% Lines 24/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                      5x     5x     5x           48x             13x                   3x                   3x                                 40x 40x   38x   32x 32x 1x   32x       6x               5x 4x     4x         4x 3x             3x                       7x 7x 4x      
// Pre-compute and cache enriched project JSON for marketplace reads
import type { Dal } from "../dal";
import type { Services } from "../services";
import type { getDb } from "../db";
import {
	enrichProjectsWithTaxonomy,
	type ProjectWithTaxonomy,
} from "../routes/marketplace/project-enrichment";
import { cache } from "./cache";
 
/** Cache key prefix for enriched project by ID */
const PROJECT_CACHE_PREFIX = "project:enriched:";
 
/** Cache key prefix for enriched project by slug */
const PROJECT_SLUG_CACHE_PREFIX = "project:enriched:slug:";
 
/** Default TTL for enriched project cache (5 minutes) */
const PROJECT_CACHE_TTL = 300;
 
/**
 * Build the cache key for an enriched project by ID
 */
export function projectCacheKey(projectId: string): string {
	return `${PROJECT_CACHE_PREFIX}${projectId}`;
}
 
/**
 * Build the cache key for an enriched project by slug
 */
export function projectSlugCacheKey(slug: string): string {
	return `${PROJECT_SLUG_CACHE_PREFIX}${slug}`;
}
 
/**
 * Get a cached enriched project by ID.
 * Returns null on cache miss.
 */
export async function getCachedEnrichedProject(
	projectId: string,
): Promise<ProjectWithTaxonomy | null> {
	return cache.get<ProjectWithTaxonomy>(projectCacheKey(projectId));
}
 
/**
 * Get a cached enriched project by slug.
 * Returns null on cache miss.
 */
export async function getCachedEnrichedProjectBySlug(
	slug: string,
): Promise<ProjectWithTaxonomy | null> {
	return cache.get<ProjectWithTaxonomy>(projectSlugCacheKey(slug));
}
 
/**
 * Pre-compute the enriched project JSON and store in cache.
 *
 * Called after project mutations (publish, update, photo/room/media changes).
 * If the project is not published or doesn't exist, clears stale cache entries.
 *
 * Designed to be called via `c.executionCtx.waitUntil()` for non-blocking execution.
 */
export async function precomputeEnrichedProject(
	projectId: string,
	db: ReturnType<typeof getDb>,
	dal: Dal,
	services: Services,
): Promise<void> {
	try {
		const project = await dal.projects.findById(projectId);
 
		if (!project || project.status !== "published") {
			// Remove stale cache entries
			await cache.delete(projectCacheKey(projectId));
			if (project?.slug) {
				await cache.delete(projectSlugCacheKey(project.slug));
			}
			return;
		}
 
		// Use existing enrichment logic with rooms and media included
		const enriched = await enrichProjectsWithTaxonomy(
			db,
			services,
			dal,
			[project],
			true,
		);
 
		if (enriched.length > 0) {
			const enrichedProject = enriched[0];
 
			// Cache by project ID
			await cache.put(projectCacheKey(project.id), enrichedProject, {
				expirationTtl: PROJECT_CACHE_TTL,
			});
 
			// Also cache by slug for slug-based lookups
			if (project.slug) {
				await cache.put(projectSlugCacheKey(project.slug), enrichedProject, {
					expirationTtl: PROJECT_CACHE_TTL,
				});
			}
		}
	} catch (err) {
		// Log but don't throw - precompute failures should not break mutations
		console.error("Failed to precompute enriched project:", projectId, err);
	}
}
 
/**
 * Invalidate cached enriched project data (by ID and slug).
 * Useful when a project is deleted or unpublished.
 */
export async function invalidateProjectCache(
	projectId: string,
	slug?: string | null,
): Promise<void> {
	await cache.delete(projectCacheKey(projectId));
	if (slug) {
		await cache.delete(projectSlugCacheKey(slug));
	}
}