freshyo/apps/backend/src/lib/cloud_cache.ts
2026-08-08 12:40:19 +05:30

288 lines
9.9 KiB
TypeScript

import { Buffer } from 'buffer'
import { scaffoldProducts, scaffoldAvailability } from '@/src/trpc/apis/common-apis/common'
import { scaffoldEssentialConsts } from '@/src/trpc/apis/common-apis/common-trpc-index'
import { scaffoldStores } from '@/src/trpc/apis/user-apis/apis/stores'
import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots'
import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners'
import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores'
import { getStoresSummary, incrementCacheVersion, incrementAvailabilityVersionNum, incrementSlotsVersionNum } from '@/src/dbService'
import { imageUploadS3 } from '@/src/lib/s3-client'
import { getApiCacheKey, getCloudflareApiToken, getCloudflareZoneId, getAssetsDomain } from '@/src/lib/env-exporter'
import { CACHE_FILENAMES } from '@packages/shared'
import { retryWithExponentialBackoff } from '@/src/lib/retry'
const buildCachePath = (path: string, version: number) => `v-${version}/${path}`
const buildAvailabilityPath = (version: number) => `av-${version}/${CACHE_FILENAMES.availability}`
const buildSlotsPath = (version: number) => `slots/v-${version}/${CACHE_FILENAMES.slots}`
function constructCacheUrl(path: string, version: number): string {
return `${getAssetsDomain()}${getApiCacheKey()}/${buildCachePath(path, version)}`
}
function constructAvailabilityUrl(version: number): string {
return `${getAssetsDomain()}${buildAvailabilityPath(version)}`
}
function constructSlotsUrl(version: number): string {
return `${getAssetsDomain()}${buildSlotsPath(version)}`
}
export interface CreateAllCacheFilesResult {
cacheVersion: number
products: string
essentialConsts: string
stores: string
slotsVersion: number
availabilityVersion: number
banners: string
individualStores: string[]
}
export async function createAllCacheFiles(): Promise<CreateAllCacheFilesResult> {
console.log('Starting creation of all cache files...')
const cacheVersion = await incrementCacheVersion()
// Create all global cache files in parallel
const [
productsKey,
essentialConstsKey,
storesKey,
slotsVersion,
availabilityVersion,
bannersKey,
individualStoreKeys,
] = await Promise.all([
createProductsFileInternal(cacheVersion),
createEssentialConstsFileInternal(cacheVersion),
createStoresFileInternal(cacheVersion),
createSlotsCacheFile(),
createAvailabilityCacheFile(),
createBannersFileInternal(cacheVersion),
createAllStoresFilesInternal(cacheVersion),
])
const stores = await getStoresSummary()
// Collect all URLs for batch cache purge
const urls = [
constructCacheUrl(CACHE_FILENAMES.products, cacheVersion),
constructCacheUrl(CACHE_FILENAMES.essentialConsts, cacheVersion),
constructCacheUrl(CACHE_FILENAMES.stores, cacheVersion),
constructSlotsUrl(slotsVersion),
constructAvailabilityUrl(availabilityVersion),
constructCacheUrl(CACHE_FILENAMES.banners, cacheVersion),
...stores.map((store) => constructCacheUrl(`stores/${store.id}.json`, cacheVersion)),
]
// Purge all caches in one batch with retry
try {
await retryWithExponentialBackoff(() => clearUrlCache(urls))
console.log(`Cache purged for all ${urls.length} files`)
} catch (error) {
console.error(`Failed to purge cache for all files after 3 retries`, error)
}
console.log('All cache files created successfully')
return {
cacheVersion,
products: productsKey,
essentialConsts: essentialConstsKey,
stores: storesKey,
slotsVersion,
availabilityVersion,
banners: bannersKey,
individualStores: individualStoreKeys,
}
}
// Internal versions that skip cache purging (for batch operations)
async function createProductsFileInternal(version: number): Promise<string> {
const productsData = await scaffoldProducts()
const jsonContent = JSON.stringify(productsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
const filePath = `${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.products, version)}`
console.log(filePath)
return await imageUploadS3(
buffer,
'application/json',
filePath
)
}
export async function createAvailabilityCacheFile(): Promise<number> {
const version = await incrementAvailabilityVersionNum()
const availabilityData = await scaffoldAvailability()
const jsonContent = JSON.stringify(availabilityData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
const filePath = buildAvailabilityPath(version)
console.log(filePath)
await imageUploadS3(
buffer,
'application/json',
filePath
)
return version
}
async function createEssentialConstsFileInternal(version: number): Promise<string> {
const essentialConstsData = await scaffoldEssentialConsts()
const jsonContent = JSON.stringify(essentialConstsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.essentialConsts, version)}`
)
}
async function createStoresFileInternal(version: number): Promise<string> {
const storesData = await scaffoldStores()
const jsonContent = JSON.stringify(storesData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.stores, version)}`
)
}
export async function createSlotsCacheFile(): Promise<number> {
const version = await incrementSlotsVersionNum()
const slotsData = await scaffoldSlotsWithProducts()
const jsonContent = JSON.stringify(slotsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
const filePath = buildSlotsPath(version)
console.log(filePath)
await imageUploadS3(
buffer,
'application/json',
filePath
)
return version
}
async function createBannersFileInternal(version: number): Promise<string> {
const bannersData = await scaffoldBanners()
const jsonContent = JSON.stringify(bannersData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.banners, version)}`
)
}
async function createAllStoresFilesInternal(version: number): Promise<string[]> {
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { storeInfo } from '@/src/db/schema'
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
*/
const stores = await getStoresSummary()
const results: string[] = []
for (const store of stores) {
const storeData = await scaffoldStoreWithProducts(store.id)
const jsonContent = JSON.stringify(storeData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
const s3Key = await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(`stores/${store.id}.json`, version)}`
)
results.push(s3Key)
}
console.log(`Created ${results.length} store cache files`)
return results
}
export async function clearUrlCache(urls: string[]): Promise<{ success: boolean; errors?: string[] }> {
const cloudflareApiToken = getCloudflareApiToken()
const cloudflareZoneId = getCloudflareZoneId()
if (!cloudflareApiToken || !cloudflareZoneId) {
console.warn('Cloudflare credentials not configured, skipping cache clear')
return { success: false, errors: ['Cloudflare credentials not configured'] }
}
try {
const resp = await fetch(
`https://api.cloudflare.com/client/v4/zones/${cloudflareZoneId}/purge_cache`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${cloudflareApiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ files: urls }),
}
)
const result = await resp.json() as { success: boolean; errors?: { message: string }[] }
if (!result.success) {
const errorMessages = result.errors?.map(e => e.message) || ['Unknown error']
console.error(`Cloudflare cache purge failed for URLs: ${urls.join(', ')}`, errorMessages)
return { success: false, errors: errorMessages }
}
console.log(`Successfully purged ${urls.length} URLs from Cloudflare cache: ${urls.join(', ')}`)
return { success: true }
} catch (error) {
console.log(error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error(`Error clearing Cloudflare cache for URLs: ${urls.join(', ')}`, errorMessage)
return { success: false, errors: [errorMessage] }
}
}
export async function clearAllCache(): Promise<{ success: boolean; errors?: string[] }> {
const cloudflareApiToken = getCloudflareApiToken()
const cloudflareZoneId = getCloudflareZoneId()
if (!cloudflareApiToken || !cloudflareZoneId) {
console.warn('Cloudflare credentials not configured, skipping cache clear')
return { success: false, errors: ['Cloudflare credentials not configured'] }
}
try {
const resp = await fetch(
`https://api.cloudflare.com/client/v4/zones/${cloudflareZoneId}/purge_cache`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${cloudflareApiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ purge_everything: true }),
}
)
const result = await resp.json() as { success: boolean; errors?: { message: string }[] }
if (!result.success) {
const errorMessages = result.errors?.map(e => e.message) || ['Unknown error']
console.error('Cloudflare cache purge failed:', errorMessages)
return { success: false, errors: errorMessages }
}
console.log('Successfully purged all cache from Cloudflare')
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error('Error clearing Cloudflare cache:', errorMessage)
return { success: false, errors: [errorMessage] }
}
}