Skip to content

Commit db15b8b

Browse files
authored
Merge pull request #4 from Zenfulcode/cross-origin-cache-invalidation
feat(cache): Implement comprehensive cache invalidation across admin and production environments
2 parents d2308bd + 2e23cff commit db15b8b

13 files changed

Lines changed: 352 additions & 15 deletions

File tree

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Thumbs.db
5050
.git
5151
.gitignore
5252

53+
.github
54+
5355
# Docker
5456
Dockerfile
5557
.dockerignore

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ API_BASE_URL_DEV=http://localhost:6091/api
88
# Production API URL
99
API_BASE_URL_PROD=
1010

11+
STORE_FRONT_URL=http://localhost:3000
12+
# Cache Invalidation Configuration
13+
CACHE_INVALIDATION_API_KEY=cache-secret-2024
14+
1115
# Origin URL for API requests (important for CORS)
1216
ORIGIN=http://localhost:3000
1317

README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,68 @@ const newProduct = await commercify.createProduct({
167167
});
168168
```
169169

170+
## 🛍️ Setting Up a Store Front (Customer Shop)
171+
172+
This project supports running a separate customer-facing storefront alongside the admin dashboard. The storefront is a SvelteKit app that fetches product, category, and order data from the Commercify API.
173+
174+
### 1. Prerequisites
175+
- Ensure you have Docker and Docker Compose installed
176+
- The Commercify API backend must be running (see docker-compose.yml)
177+
178+
### 2. Environment Configuration
179+
Create a `.env` file for your storefront (or use Docker Compose environment variables):
180+
181+
```env
182+
NODE_ENV=production
183+
API_BASE_URL_PROD=http://commercify-api:6091
184+
CACHE_INVALIDATION_API_KEY=your-cache-secret
185+
ORIGIN=http://localhost:3000
186+
```
187+
188+
### 3. Docker Compose Setup
189+
Add a service for your storefront in `docker-compose.yml`:
190+
191+
```yaml
192+
storefront-app:
193+
build:
194+
context: .
195+
dockerfile: Dockerfile
196+
ports:
197+
- '3000:3000'
198+
environment:
199+
- NODE_ENV=production
200+
- ORIGIN=http://localhost:3000
201+
- CACHE_INVALIDATION_API_KEY=your-cache-secret
202+
- API_BASE_URL_PROD=http://commercify-api:6091
203+
depends_on:
204+
commercify-api:
205+
condition: service_healthy
206+
postgres:
207+
condition: service_healthy
208+
restart: unless-stopped
209+
networks:
210+
- hh-commercify-network
211+
```
212+
213+
### 4. Running the Storefront
214+
215+
Start all services:
216+
```bash
217+
docker-compose up -d
218+
```
219+
220+
The storefront will be available at `http://localhost:3000`.
221+
222+
### 5. Cache Invalidation
223+
224+
When you update products or categories in the admin dashboard, cache invalidation requests are sent to the storefront (`storefront-app`) to ensure customers see fresh data immediately. Make sure the storefront implements the `/api/cache/invalidate` endpoint for this to work.
225+
226+
### 6. Customizing the Storefront
227+
- Update branding, theme, and layout in the `src/routes` and `src/lib/components/ui` folders
228+
- Configure payment, shipping, and other integrations as needed
229+
230+
---
231+
170232
## 🤝 Contributing
171233

172234
1. Fork the repository

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ services:
9595
- HOST=0.0.0.0
9696
- API_BASE_URL_DEV=http://commercify-api:6091
9797
- API_BASE_URL_PROD=http://commercify-api:6091
98+
- CACHE_INVALIDATION_API_KEY=${CACHE_INVALIDATION_API_KEY:-your-secret-key}
99+
- STORE_FRONT_URL=${STORE_FRONT_URL:-http://localhost:3000}
98100
depends_on:
99101
- commercify-api
100102
restart: unless-stopped
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Cache invalidation utility for coordinating cache clearing between applications
3+
*/
4+
5+
import { env } from '$env/dynamic/private';
6+
7+
const CACHE_API_KEY = env.CACHE_INVALIDATION_API_KEY || 'your-secret-key';
8+
9+
const PRODUCTION_APP_URL = env.STORE_FRONT_URL || 'http://localhost:3000';
10+
11+
interface CacheInvalidationRequest {
12+
type: 'product' | 'category' | 'all';
13+
id?: string | number;
14+
apiKey: string;
15+
}
16+
17+
/**
18+
* Invalidates cache in the production application by calling its API endpoint
19+
*/
20+
export async function invalidateProductionCache(
21+
type: 'product' | 'category' | 'all',
22+
id?: string | number
23+
): Promise<void> {
24+
try {
25+
const requestData: CacheInvalidationRequest = {
26+
type,
27+
id,
28+
apiKey: CACHE_API_KEY
29+
};
30+
31+
console.log(`[Cache Coordination] Targeting storefront app: ${PRODUCTION_APP_URL}`);
32+
console.log(`[Cache Coordination] Sending cache invalidation request: type=${type}, id=${id}`);
33+
34+
const response = await fetch(`${PRODUCTION_APP_URL}/api/cache/invalidate`, {
35+
method: 'POST',
36+
headers: {
37+
'Content-Type': 'application/json'
38+
},
39+
body: JSON.stringify(requestData),
40+
// Add timeout to prevent hanging
41+
signal: AbortSignal.timeout(5000) // 5 second timeout
42+
});
43+
44+
if (!response.ok) {
45+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
46+
console.error(
47+
'[Cache Coordination] Failed to invalidate production cache:',
48+
response.status,
49+
error
50+
);
51+
return;
52+
}
53+
54+
const result = await response.json();
55+
console.log('[Cache Coordination] Production cache invalidation successful:', result);
56+
} catch (error) {
57+
if (error instanceof Error && error.name === 'TimeoutError') {
58+
console.warn(
59+
'[Cache Coordination] Timeout calling production cache invalidation API - production app may not be running'
60+
);
61+
} else if (error instanceof Error && error.message.includes('ECONNREFUSED')) {
62+
console.warn('[Cache Coordination] Cannot connect to production app - it may not be running');
63+
} else {
64+
console.error('[Cache Coordination] Error calling production cache invalidation API:', error);
65+
}
66+
// Don't throw the error - cache invalidation should be non-blocking
67+
}
68+
}
69+
70+
/**
71+
* Convenience method for invalidating product cache in production
72+
*/
73+
export async function invalidateProductionProductCache(id?: string | number): Promise<void> {
74+
await invalidateProductionCache('product', id);
75+
}
76+
77+
/**
78+
* Convenience method for invalidating category cache in production
79+
*/
80+
export async function invalidateProductionCategoryCache(id?: string | number): Promise<void> {
81+
await invalidateProductionCache('category', id);
82+
}
83+
84+
/**
85+
* Convenience method for invalidating all caches in production
86+
*/
87+
export async function invalidateAllProductionCaches(): Promise<void> {
88+
await invalidateProductionCache('all');
89+
}

src/lib/server/commercify/api.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,21 @@ export class CachedCommercifyApiClient {
153153
};
154154

155155
const result = await this.client.products.update(id, requestData, productResponseMapper);
156+
157+
console.log(`[API] Product ${id} updated, triggering cache invalidation`);
158+
console.log(`[API] isActive changed:`, data.isActive !== undefined);
159+
160+
// Comprehensive cache invalidation for product updates
161+
// This is especially important when changing a product's active status
156162
await CacheInvalidator.invalidateAllProductCaches(id);
163+
164+
// If the active status might have changed, also invalidate category caches
165+
// since category product counts might be affected
166+
if (data.isActive !== undefined) {
167+
console.log(`[API] Active status change detected, clearing category caches`);
168+
await CacheInvalidator.invalidateAllCategoryCaches();
169+
}
170+
157171
return result;
158172
},
159173

src/lib/server/commercify/cache.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { dev } from '$app/environment';
2-
import { OrderCache, ProductCache } from '$lib/cache';
2+
import { OrderCache, ProductCache, CheckoutCache, Cache } from '$lib/cache';
33

44
// Server-side cache implementation
55
interface ServerCacheEntry<T> {
@@ -68,16 +68,31 @@ class ServerCache {
6868
}
6969

7070
invalidate(key: string): void {
71+
if (this.cache.has(key)) {
72+
console.log(`[ServerCache] Invalidating cache key: ${key}`);
73+
}
7174
this.cache.delete(key);
7275
}
7376

7477
invalidatePattern(pattern: string): void {
7578
const regex = new RegExp(pattern);
79+
const keysToDelete: string[] = [];
80+
7681
for (const key of this.cache.keys()) {
7782
if (regex.test(key)) {
78-
this.cache.delete(key);
83+
keysToDelete.push(key);
7984
}
8085
}
86+
87+
if (keysToDelete.length > 0) {
88+
console.log(
89+
`[ServerCache] Invalidating ${keysToDelete.length} cache entries matching pattern: ${pattern}`
90+
);
91+
keysToDelete.forEach((key) => {
92+
console.log(`[ServerCache] Deleting key: ${key}`);
93+
this.cache.delete(key);
94+
});
95+
}
8196
}
8297

8398
clear(): void {
@@ -243,6 +258,7 @@ export class CacheInvalidator {
243258
*/
244259
static async invalidateProduct(id: string | number): Promise<void> {
245260
const productId = id.toString();
261+
console.log(`[CacheInvalidator] Invalidating product cache for ID: ${productId}`);
246262

247263
// Client-side cache invalidation
248264
ProductCache.invalidateProduct(productId);
@@ -255,6 +271,8 @@ export class CacheInvalidator {
255271
* Invalidates all product-related caches (lists, search results, etc.)
256272
*/
257273
static async invalidateProductLists(): Promise<void> {
274+
console.log('[CacheInvalidator] Invalidating all product lists cache');
275+
258276
// Client-side cache invalidation
259277
ProductCache.invalidateProducts();
260278

@@ -266,6 +284,8 @@ export class CacheInvalidator {
266284
* Invalidates both individual product and product lists caches
267285
*/
268286
static async invalidateAllProductCaches(id?: string | number): Promise<void> {
287+
console.log(`[CacheInvalidator] Invalidating ALL product caches${id ? ` for ID: ${id}` : ''}`);
288+
269289
if (id !== undefined) {
270290
await this.invalidateProduct(id);
271291
}
@@ -290,13 +310,33 @@ export class CacheInvalidator {
290310
}
291311

292312
/**
293-
* Invalidates all order-related caches (lists, etc.)
313+
* Invalidates all order-related caches (lists, search results, etc.)
294314
*/
295315
static async invalidateOrderLists(): Promise<void> {
316+
console.log('[CacheInvalidator] Invalidating all order lists cache');
317+
318+
// Client-side cache invalidation
319+
OrderCache.clear();
320+
296321
// Server-side cache invalidation
297322
serverCache.invalidatePattern('^orders:');
298323
}
299324

325+
/**
326+
* Invalidates ALL caches - use with caution, this is a nuclear option
327+
*/
328+
static invalidateAllCaches(): void {
329+
console.log('[CacheInvalidator] Clearing ALL caches (nuclear option)');
330+
331+
// Clear server-side cache completely
332+
serverCache.clear();
333+
334+
// Clear client-side caches
335+
Cache.clear();
336+
ProductCache.clear();
337+
OrderCache.clear();
338+
CheckoutCache.clear();
339+
}
300340
/**
301341
* Invalidates both client-side and server-side caches for a specific category
302342
*/

src/routes/admin/products/+page.server.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,28 @@ export const actions: Actions = {
5252
});
5353
}
5454

55+
// Comprehensive cache invalidation after product deletion
56+
const { serverCache, CacheInvalidator } = await import('$lib/server/commercify/cache');
57+
const { invalidateProductionProductCache, invalidateProductionCategoryCache } = await import(
58+
'$lib/server/cache-coordination'
59+
);
60+
61+
// Clear all product-related caches in admin application
62+
await CacheInvalidator.invalidateAllProductCaches(productId);
63+
64+
// Clear category caches since product counts might be affected
65+
await CacheInvalidator.invalidateAllCategoryCaches();
66+
67+
// Additional server cache clearing
68+
serverCache.invalidatePattern('^products:');
69+
serverCache.invalidatePattern('^product:');
70+
serverCache.invalidatePattern('^categor');
71+
72+
// IMPORTANT: Also invalidate cache in the production application
73+
console.log('[Admin] Invalidating production application cache after deletion...');
74+
await invalidateProductionProductCache(productId);
75+
await invalidateProductionCategoryCache();
76+
5577
return {
5678
success: true,
5779
message: 'Product deleted successfully'

src/routes/admin/products/[id]/edit/+page.server.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const load = async ({ params, locals, url }) => {
1111
try {
1212
// Check if we should bypass cache (e.g., after an update)
1313
const bypassCache = url.searchParams.has('refresh');
14-
14+
1515
// Get the existing product data
1616
const result = await commercify.products.get(productId, bypassCache);
1717

@@ -111,10 +111,34 @@ export const actions: Actions = {
111111
});
112112
}
113113

114-
// Additional cache invalidation to ensure fresh data
115-
const { serverCache } = await import('$lib/server/commercify/cache');
116-
serverCache.invalidate(`product:${productId}`);
114+
// Additional comprehensive cache invalidation to ensure fresh data
115+
// This is especially important for active status changes
116+
const { serverCache, CacheInvalidator } = await import('$lib/server/commercify/cache');
117+
const { invalidateProductionProductCache, invalidateProductionCategoryCache } = await import(
118+
'$lib/server/cache-coordination'
119+
);
120+
121+
// Clear all product-related caches in the admin application
122+
await CacheInvalidator.invalidateAllProductCaches(productId);
123+
124+
// If the active status was potentially changed, also clear category caches
125+
// since category product counts might be affected
126+
if (form.data.isActive !== undefined) {
127+
await CacheInvalidator.invalidateAllCategoryCaches();
128+
}
129+
130+
// Additional server cache clearing for good measure
117131
serverCache.invalidatePattern('^products:');
132+
serverCache.invalidatePattern('^product:');
133+
serverCache.invalidatePattern('^categor'); // Clear category caches too
134+
135+
// IMPORTANT: Also invalidate cache in the production application
136+
console.log('[Admin] Invalidating production application cache...');
137+
await invalidateProductionProductCache(productId);
138+
139+
if (form.data.isActive !== undefined) {
140+
await invalidateProductionCategoryCache();
141+
}
118142

119143
// Invalidate all cached data to ensure fresh product data is loaded
120144
redirect(303, '/admin/products');

0 commit comments

Comments
 (0)