Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/common/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@stellar-pay/common",
"version": "0.1.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean"
},
"dependencies": {
"ioredis": "^5.3.2"
},
"devDependencies": {
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}
144 changes: 144 additions & 0 deletions packages/common/src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Redis, { RedisOptions } from 'ioredis';

// --- Redis Client Configuration ---
const redisOptions: RedisOptions = {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
password: process.env.REDIS_PASSWORD,
db: parseInt(process.env.REDIS_DB || '0', 10),
keyPrefix: process.env.REDIS_PREFIX || 'stellar-pay:',
retryStrategy: (times) => {
return Math.min(times * 50, 2000);
},
};

// Use REDIS_URL if provided, else fallback to individual options
export const redisClient = new Redis(process.env.REDIS_URL || redisOptions);

redisClient.on('connect', () => {
console.log('[Redis] Connected gracefully');
});

redisClient.on('error', (err) => {
console.error('[Redis] Error connecting: ', err);
});

// --- Cache Decorators ---

/**
* Cacheable Decorator for expensive operations
* @param keyPrefix Prefix for the cache key
* @param ttl Time to live in seconds (default 300)
*/
export function Cacheable(keyPrefix: string, ttl: number = 300) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = async function (...args: any[]) {
const key = `${keyPrefix}:${args.join(':')}`;

try {
const cachedValue = await redisClient.get(key);

if (cachedValue) {
console.log(`[Cache Hit] key: ${key}`);
return JSON.parse(cachedValue);
}

console.log(`[Cache Miss] key: ${key}`);
const result = await originalMethod.apply(this, args);

if (result !== undefined && result !== null) {
await redisClient.set(key, JSON.stringify(result), 'EX', ttl);
}

return result;
} catch (error) {
console.error(`[Cache Error] failed to process key: ${key}`, error);
return await originalMethod.apply(this, args);
}
};

return descriptor;
};
}

/**
* CacheInvalidate Decorator for clearing cache after updates
* @param keyPrefix Prefix for the cache key to invalidate
*/
export function CacheInvalidate(keyPrefix: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = async function (...args: any[]) {
// Assuming the first argument is the identifier (like transactionId)
const id = args[0];
const key = id ? `${keyPrefix}:${id}` : keyPrefix;

const result = await originalMethod.apply(this, args);

try {
await redisClient.del(key);
console.log(`[Cache Invalidation] cleared key: ${key}`);
} catch (error) {
console.error(`[Cache Error] failed to invalidate key: ${key}`, error);
}

return result;
};

return descriptor;
};
}

// --- Specific Transaction Cache Class implementation ---

export class TransactionStatusCache {
private static PREFIX = 'tx_status';
private static TTL = 60;

static async getStatus(transactionId: string): Promise<string | null> {
const key = `${this.PREFIX}:${transactionId}`;
try {
const value = await redisClient.get(key);
if (value) {
console.log(`[Cache Hit] Transaction Status Lookup - key: ${key}`);
return value;
}
console.log(`[Cache Miss] Transaction Status Lookup - key: ${key}`);
return null;
} catch (err) {
console.error(`[Cache Error] getStatus: ${key}`, err);
return null;
}
}

static async setStatus(transactionId: string, status: string): Promise<void> {
const key = `${this.PREFIX}:${transactionId}`;
try {
await redisClient.set(key, status, 'EX', this.TTL);
console.log(`[Cache Set] Transaction Status stored - key: ${key}`);
} catch (err) {
console.error(`[Cache Error] setStatus: ${key}`, err);
}
}

static async invalidateStatus(transactionId: string): Promise<void> {
const key = `${this.PREFIX}:${transactionId}`;
try {
await redisClient.del(key);
console.log(`[Cache Invalidation] Transaction Status cleared - key: ${key}`);
} catch (err) {
console.error(`[Cache Error] invalidateStatus: ${key}`, err);
}
}
}
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './cache';
52 changes: 52 additions & 0 deletions packages/common/test-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { redisClient, Cacheable, CacheInvalidate, TransactionStatusCache } from './src/cache';

class TestService {
@Cacheable('expensive_op')
async getExpensiveData(id: string) {
console.log(`[TestService] Fetching Expensive Data for ${id}...`);
return { data: `Expensive result ${id}` };
}

@CacheInvalidate('expensive_op')
async updateData(id: string) {
console.log(`[TestService] Updating Data for ${id}...`);
return { success: true };
}
}

async function runTest() {
const service = new TestService();

console.log('--- Testing Decorators ---');
// 1st call: Cache Miss
await service.getExpensiveData('123');
// 2nd call: Cache Hit
await service.getExpensiveData('123');

// Update data: Invalidate Cache
await service.updateData('123');

// 3rd call: Cache Miss again
await service.getExpensiveData('123');

console.log('\n--- Testing Transaction Status Cache ---');
// 1st call: Cache Miss
await TransactionStatusCache.getStatus('TX_999');

// Set cache
await TransactionStatusCache.setStatus('TX_999', 'COMPLETED');

// 2nd call: Cache Hit
await TransactionStatusCache.getStatus('TX_999');

// Invalidate cache
await TransactionStatusCache.invalidateStatus('TX_999');

// 3rd call: Cache Miss
await TransactionStatusCache.getStatus('TX_999');

// Close redis connection to exit the script
await redisClient.quit();
}

runTest().catch(console.error);
Loading